前言一:接下來一段時間我會陸續更新一些列Flutter文字教程vue
更新進度: 每週至少兩篇;ios
更新地點: 首發於公衆號,次日更新於掘金、思否、開發者頭條等地方;算法
更多交流: 能夠添加個人微信 372623326,關注個人微博:coderwhyjson
但願你們能夠 幫忙轉發,點擊在看,給我更多的創做動力。安全
前言二:列表是移動端常用的一種視圖展現方式,在Flutter中提供了ListView和GridView。服務器
爲了可能展現出更好的效果,我這裏提供了一段Json數據,因此咱們能夠先學習一下Json解析。微信
在開發中,咱們常常會使用本地JSON或者從服務器請求數據後回去到JSON,拿到JSON後一般會將JSON轉成Model對象來進行後續的操做,由於這樣操做更加的方便,也更加的安全。數據結構
因此學習JSON的相關操做以及讀取JSON後如何轉成Model對象對於Flutter開發也很是重要。app
JSON也屬於一種資源,因此在使用以前須要先進行相關的配置less
咱們以前在學習使用Image組件時,用到了本地圖片,本地圖片必須在pubspec.yaml
中進行配置:
JSON資源讀取
若是咱們但願讀取JSON資源,可使用package:flutter/services.dart
包中的rootBundle
。
在rootBundle
中有一個loadString
方法,能夠去加載JSON資源
Future<String> loadString(String key, { bool cache = true }) async {
...省略具體代碼,能夠自行查看源碼
}
複製代碼
代碼以下:(不要試圖拷貝這個代碼去運行,是沒辦法運行的)
import 'package:flutter/services.dart' show rootBundle;
// 打印讀取的結果是一個字符串
rootBundle.loadString("assets/yz.json").then((value) => print(value));
複製代碼
JSON字符串轉化
拿到JSON字符串後,咱們須要將其轉成成咱們熟悉的List和Map類型。
咱們能夠經過dart:convert
包中的json.decode
方法將其進行轉化
代碼以下:
// 1.讀取json文件
String jsonString = await rootBundle.loadString("assets/yz.json");
// 2.轉成List或Map類型
final jsonResult = json.decode(jsonString);
複製代碼
對象Model定義
將JSON轉成了List和Map類型後,就能夠將List中的一個個Map轉成Model對象,因此咱們須要定義本身的Model
class Anchor {
String nickname;
String roomName;
String imageUrl;
Anchor({
this.nickname,
this.roomName,
this.imageUrl
});
Anchor.withMap(Map<String, dynamic> parsedMap) {
this.nickname = parsedMap["nickname"];
this.roomName = parsedMap["roomName"];
this.imageUrl = parsedMap["roomSrc"];
}
}
複製代碼
上面咱們給出瞭解析的一個個步驟,下面咱們給出完整的代碼邏輯
這裏我單首創建了一個anchor.dart的文件,在其中定義了全部的相關代碼:
getAnchors
就能夠獲取到解析後的數據了import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
class Anchor {
String nickname;
String roomName;
String imageUrl;
Anchor({
this.nickname,
this.roomName,
this.imageUrl
});
Anchor.withMap(Map<String, dynamic> parsedMap) {
this.nickname = parsedMap["nickname"];
this.roomName = parsedMap["roomName"];
this.imageUrl = parsedMap["roomSrc"];
}
}
Future<List<Anchor>> getAnchors() async {
// 1.讀取json文件
String jsonString = await rootBundle.loadString("assets/yz.json");
// 2.轉成List或Map類型
final jsonResult = json.decode(jsonString);
// 3.遍歷List,而且轉成Anchor對象放到另外一個List中
List<Anchor> anchors = new List();
for (Map<String, dynamic> map in jsonResult) {
anchors.add(Anchor.withMap(map));
}
return anchors;
}
複製代碼
移動端數據量比較大時,咱們都是經過列表來進行展現的,好比商品數據、聊天列表、通訊錄、朋友圈等。
在Android中,咱們可使用ListView或RecyclerView來實現,在iOS中,咱們能夠經過UITableView來實現。
在Flutter中,咱們也有對應的列表Widget,就是ListView。
ListView能夠沿一個方向(垂直或水平方向,默認是垂直方向)來排列其全部子Widget。
一種最簡單的使用方式是直接將全部須要排列的子Widget放在ListView的children屬性中便可。
咱們來看一下直接使用ListView的代碼演練:
class MyHomeBody extends StatelessWidget {
final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("人的一切痛苦,本質上都是對本身無能的憤怒。", style: textStyle),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("人活在世界上,不能夠有誤差;並且多少要費點勁兒,才能把本身保持到理性的軌道上。", style: textStyle),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("我活在世上,無非想要明白些道理,碰見些有趣的事。", style: textStyle),
)
],
);
}
}
複製代碼
在開發中,咱們常常見到一種列表,有一個圖標或圖片(Icon),有一個標題(Title),有一個子標題(Subtitle),還有尾部一個圖標(Icon)。
這個時候,咱們可使用ListTile來實現:
class MyHomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.people, size: 36,),
title: Text("聯繫人"),
subtitle: Text("聯繫人信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.email, size: 36,),
title: Text("郵箱"),
subtitle: Text("郵箱地址信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.message, size: 36,),
title: Text("消息"),
subtitle: Text("消息詳情信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.map, size: 36,),
title: Text("地址"),
subtitle: Text("地址詳情信息"),
trailing: Icon(Icons.arrow_forward_ios),
)
],
);
}
}
複製代碼
咱們能夠經過設置 scrollDirection
參數來控制視圖的滾動方向。
咱們經過下面的代碼實現一個水平滾動的內容:
class MyHomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.horizontal,
itemExtent: 200,
children: <Widget>[
Container(color: Colors.red, width: 200),
Container(color: Colors.green, width: 200),
Container(color: Colors.blue, width: 200),
Container(color: Colors.purple, width: 200),
Container(color: Colors.orange, width: 200),
],
);
}
}
複製代碼
經過構造函數中的children傳入全部的子Widget有一個問題:默認會建立出全部的子Widget。
可是對於用戶來講,一次性構建出全部的Widget並不會有什麼差別,可是對於咱們的程序來講會產生性能問題,並且會增長首屏的渲染時間。
咱們能夠ListView.build來構建子Widget,提供性能。
ListView.build適用於子Widget比較多的場景,該構造函數將建立子Widget交給了一個抽象的方法,交給ListView進行管理,ListView會在真正須要的時候去建立子Widget,而不是一開始就所有初始化好。
該方法有兩個重要參數:
咱們仍是經過一個簡單的案例來認識它:
class MyHomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 100,
itemExtent: 80,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("標題$index"), subtitle: Text("詳情內容$index"));
}
);
}
}
複製代碼
在以前,咱們搞了一個yz.json數據,咱們如今動態的來經過JSON數據展現一個列表。
思考:這個時候是否依然可使用StatelessWidget
:
答案:不能夠,由於當前咱們的數據是異步加載的,剛開始界面並不會展現數據(沒有數據),後面從JSON中加載出來數據(有數據)後,再次展現加載的數據。
StatefulWidget
來管理組件。展現代碼以下:
import 'model/anchor.dart';
...省略中間代碼
class MyHomeBody extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return MyHomeBodyState();
}
}
class MyHomeBodyState extends State<MyHomeBody> {
List<Anchor> anchors = [];
// 在初始化狀態的方法中加載數據
@override
void initState() {
getAnchors().then((anchors) {
setState(() {
this.anchors = anchors;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.network(
anchors[index].imageUrl,
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
SizedBox(height: 8),
Text(anchors[index].nickname, style: TextStyle(fontSize: 20),),
SizedBox(height: 5),
Text(anchors[index].roomName)
],
),
);
},
);
}
}
複製代碼
ListView.separated
能夠生成列表項之間的分割器,它除了比ListView.builder
多了一個separatorBuilder
參數,該參數是一個分割器生成器。
下面咱們看一個例子:奇數行添加一條藍色下劃線,偶數行添加一條紅色下劃線:
class MySeparatedDemo extends StatelessWidget {
Divider blueColor = Divider(color: Colors.blue);
Divider redColor = Divider(color: Colors.red);
@override
Widget build(BuildContext context) {
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Icon(Icons.people),
title: Text("聯繫人${index+1}"),
subtitle: Text("聯繫人電話${index+1}"),
);
},
separatorBuilder: (BuildContext context, int index) {
return index % 2 == 0 ? redColor : blueColor;
},
itemCount: 100
);
}
}
複製代碼
GridView用於展現多列的展現,在開發中也很是常見,好比直播App中的主播列表、電商中的商品列表等等。
在Flutter中咱們可使用GridView來實現,使用方式和ListView也比較類似。
咱們先學習GridView構造函數的使用方法
一種使用GridView的方式就是使用構造函數來建立,和ListView對比有一個特殊的參數:gridDelegate
gridDelegate
用於控制交叉軸的item數量或者寬度,須要傳入的類型是SliverGridDelegate,可是它是一個抽象類,因此咱們須要傳入它的子類:
SliverGridDelegateWithFixedCrossAxisCount
SliverGridDelegateWithFixedCrossAxisCount({
@required double crossAxisCount, // 交叉軸的item個數
double mainAxisSpacing = 0.0, // 主軸的間距
double crossAxisSpacing = 0.0, // 交叉軸的間距
double childAspectRatio = 1.0, // 子Widget的寬高比
})
複製代碼
代碼演練:
class MyGridCountDemo extends StatelessWidget {
List<Widget> getGridWidgets() {
return List.generate(100, (index) {
return Container(
color: Colors.purple,
alignment: Alignment(0, 0),
child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
);
});
}
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.0
),
children: getGridWidgets(),
);
}
}
複製代碼
SliverGridDelegateWithMaxCrossAxisExtent
SliverGridDelegateWithMaxCrossAxisExtent({
double maxCrossAxisExtent, // 交叉軸的item寬度
double mainAxisSpacing = 0.0, // 主軸的間距
double crossAxisSpacing = 0.0, // 交叉軸的間距
double childAspectRatio = 1.0, // 子Widget的寬高比
})
複製代碼
代碼演練:
class MyGridExtentDemo extends StatelessWidget {
List<Widget> getGridWidgets() {
return List.generate(100, (index) {
return Container(
color: Colors.purple,
alignment: Alignment(0, 0),
child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
);
});
}
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 150,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.0
),
children: getGridWidgets(),
);
}
}
複製代碼
前面兩種方式也能夠不設置delegate
能夠分別使用:GridView.count構造函數
和GridView.extent
構造函數實現相同的效果,這裏再也不贅述。
和ListView同樣,使用構造函數會一次性建立全部的子Widget,會帶來性能問題,因此咱們可使用GridView.build
來交給GridView本身管理須要建立的子Widget。
咱們直接使用以前的數據來進行代碼演練:
class _GridViewBuildDemoState extends State<GridViewBuildDemo> {
List<Anchor> anchors = [];
@override
void initState() {
getAnchors().then((anchors) {
setState(() {
this.anchors = anchors;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.2
),
itemCount: anchors.length,
itemBuilder: (BuildContext context, int index) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.network(anchors[index].imageUrl),
SizedBox(height: 5),
Text(anchors[index].nickname, style: TextStyle(fontSize: 16),),
Text(anchors[index].roomName, maxLines: 1, overflow: TextOverflow.ellipsis,)
],
),
);
}
),
);
}
}
複製代碼
咱們考慮一個這樣的佈局:一個滑動的視圖中包括一個標題視圖(HeaderView),一個列表視圖(ListView),一個網格視圖(GridView)。
咱們怎麼可讓它們作到統一的滑動效果呢?使用前面的滾動是很難作到的。
Flutter中有一個能夠完成這樣滾動效果的Widget:CustomScrollView,能夠統一管理多個滾動視圖。
在CustomScrollView中,每個獨立的,可滾動的Widget被稱之爲Sliver。
補充:Sliver能夠翻譯成裂片、薄片,你能夠將每個獨立的滾動視圖當作一個小裂片。
由於咱們須要把不少的Sliver放在一個CustomScrollView中,因此CustomScrollView有一個slivers屬性,裏面讓咱們放對應的一些Sliver:
咱們簡單演示一下:SliverGrid+SliverPadding+SliverSafeArea的組合
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
SliverSafeArea(
sliver: SliverPadding(
padding: EdgeInsets.all(8),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment(0, 0),
color: Colors.orange,
child: Text("item$index"),
);
},
childCount: 20
),
),
),
)
],
);
}
}
複製代碼
這裏我使用官方的示例程序,將SliverAppBar+SliverGrid+SliverFixedExtentList作出以下界面:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return showCustomScrollView();
}
Widget showCustomScrollView() {
return new CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('Coderwhy Demo'),
background: Image(
image: NetworkImage(
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg",
),
fit: BoxFit.cover,
),
),
),
new SliverGrid(
gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.teal[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 10,
),
),
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
childCount: 20
),
),
],
);
}
}
複製代碼
對於滾動的視圖,咱們常常須要監聽它的一些滾動事件,在監聽到的時候去作對應的一些事情。
好比視圖滾動到底部時,咱們可能但願作上拉加載更多;
好比滾動到必定位置時顯示一個回到頂部的按鈕,點擊回到頂部的按鈕,回到頂部;
好比監聽滾動何時開始,何時結束;
在Flutter中監聽滾動相關的內容由兩部分組成:ScrollController和ScrollNotification。
在Flutter中,Widget並非最終渲染到屏幕上的元素(真正渲染的是RenderObject),所以一般這種監聽事件以及相關的信息並不能直接從Widget中獲取,而是必須經過對應的Widget的Controller來實現。
ListView、GridView的組件控制器是ScrollController,咱們能夠經過它來獲取視圖的滾動信息,而且能夠調用裏面的方法來更新視圖的滾動位置。
另外,一般狀況下,咱們會根據滾動的位置來改變一些Widget的狀態信息,因此ScrollController一般會和StatefulWidget一塊兒來使用,而且會在其中控制它的初始化、監聽、銷燬等事件。
咱們來作一個案例,當滾動到1000位置的時候,顯示一個回到頂部的按鈕:
jumpTo(double offset)
、animateTo(double offset,...)
:這兩個方法用於跳轉到指定的位置,它們不一樣之處在於,後者在跳轉時會執行一個動畫,而前者不會。class MyHomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
ScrollController _controller;
bool _isShowTop = false;
@override
void initState() {
// 初始化ScrollController
_controller = ScrollController();
// 監聽滾動
_controller.addListener(() {
var tempSsShowTop = _controller.offset >= 1000;
if (tempSsShowTop != _isShowTop) {
setState(() {
_isShowTop = tempSsShowTop;
});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("ListView展現"),
),
body: ListView.builder(
itemCount: 100,
itemExtent: 60,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("item$index"));
}
),
floatingActionButton: !_isShowTop ? null : FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
_controller.animateTo(0, duration: Duration(milliseconds: 1000), curve: Curves.ease);
},
),
);
}
}
複製代碼
若是咱們但願監聽何時開始滾動,何時結束滾動,這個時候咱們能夠經過NotificationListener
。
true
時,則冒泡終止,事件中止向上傳播,若是不返回或者返回值爲false
時,則冒泡繼續。案例: 列表滾動, 而且在中間顯示滾動進度
class MyHomeNotificationDemo extends StatefulWidget {
@override
State<StatefulWidget> createState() => MyHomeNotificationDemoState();
}
class MyHomeNotificationDemoState extends State<MyHomeNotificationDemo> {
int _progress = 0;
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification notification) {
// 1.判斷監聽事件的類型
if (notification is ScrollStartNotification) {
print("開始滾動.....");
} else if (notification is ScrollUpdateNotification) {
// 當前滾動的位置和總長度
final currentPixel = notification.metrics.pixels;
final totalPixel = notification.metrics.maxScrollExtent;
double progress = currentPixel / totalPixel;
setState(() {
_progress = (progress * 100).toInt();
});
print("正在滾動:${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}");
} else if (notification is ScrollEndNotification) {
print("結束滾動....");
}
return false;
},
child: Stack(
alignment: Alignment(.9, .9),
children: <Widget>[
ListView.builder(
itemCount: 100,
itemExtent: 60,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("item$index"));
}
),
CircleAvatar(
radius: 30,
child: Text("$_progress%"),
backgroundColor: Colors.black54,
)
],
),
);
}
}
複製代碼
備註:全部內容首發於公衆號,以後除了Flutter也會更新其餘技術文章,TypeScript、React、Node、uniapp、mpvue、數據結構與算法等等,也會更新一些本身的學習心得等,歡迎你們關注