前一段時間項目集成了
Flutter
作了許多的功能模塊,再加上好久沒有文章產出,因此打算寫這麼一篇文章來總結和記錄Flutter
開發中的一些問題
Demo地址:github.com/weibindev/f…git
ps
:demo中的數據都從assets\data\
文件夾下的json文件讀取,因此並無涉及到網絡請求封裝,項目架構等相關知識,這個demo偏注重於點單結構的實現。github
整體的效果以下所示:json
首頁的店鋪入口沒什麼好說的,它主要是咱們點單功能的入口和店鋪購物車商品數的展現。bash
下面咱們主要來分析下點單界面的結構組成。markdown
根據上面這張圖,按照數字標識框出的地方分析以下:網絡
Android
中的statusBar
+toolbar
overlays
屬性的控件除外)其中1,2,3,4能夠看做一個總體,5能夠看做一個總體。架構
關於底部購物車,我剛開始的實現思路是用Overlay
去作,源碼中對它的描述以下app
/// A [Stack] of entries that can be managed independently.
///
/// Overlays let independent child widgets "float" visual elements on top of
/// other widgets by inserting them into the overlay's [Stack]. The overlay lets
/// each of these widgets manage their participation in the overlay using
/// [OverlayEntry] objects.
///
/// Although you can create an [Overlay] directly, it's most common to use the
/// overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The
/// navigator uses its overlay to manage the visual appearance of its routes.
///
/// See also:
///
/// * [OverlayEntry].
/// * [OverlayState].
/// * [WidgetsApp].
/// * [MaterialApp].
class Overlay extends StatefulWidget {
複製代碼
意思是Overlay
是一個Stack
組件,能夠將OverlayEntry
插入到Overlay
中,使其獨立的child
窗口懸浮於其它組件之上,利用這個特性咱們能夠用Overlay
將底部購物車組件包裹起來,覆蓋在其它的組件之上。less
然而實際使用過程當中問題多多,須要本身精準的控制好Overlay
包裹的懸浮控件的顯隱等,否則人家都退出這個界面了,我們的購物車還擱下面顯示着。我的認爲這玩意仍是更適合Popupindow
和全局自定義Dialog
之類的。ide
那麼Flutter
中有沒有方便管理一堆子組件的組件呢?
在編寫Flutter
應用的時候,咱們程序的入口是經過main()
函數的runApp(MyApp())
執行的,MyApp
一般會build
出一個MaterialApp
組件
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '我要點東西',
home: HomePage(),
);
}
}
複製代碼
對於不一樣界面之間的路由咱們會交由Navigator
管理,好比: Navigator.push
和 Navigator.pop
等。爲何MaterialApp
可以對Navigator
的操做做出感應呢?
MaterialApp
的構造方法中有這麼一個字段navigatorKey
class MaterialApp extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKey;
///省略一些代碼
}
class _MaterialAppState extends State<MaterialApp> {
@override
Widget build(BuildContext context) {
Widget result = WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: _navigatorObservers,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
return MaterialPageRoute<T>(settings: settings, builder: builder);
},
///省略一些代碼
}
}
複製代碼
往深刻的去看它會傳遞給WidgetsApp
構造方法中的navigatorKey
,WidgetsApp
的navigatorKey
在組件初始化時會默認的建立一個全局的NavigatorState
,而後對build(BuildContext context)
中建立的Navigator
進行狀態管理。
class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
_updateNavigator();
_locale = _resolveLocales(WidgetsBinding.instance.window.locales, widget.supportedLocales);
WidgetsBinding.instance.addObserver(this);
}
// NAVIGATOR
GlobalKey<NavigatorState> _navigator;
void _updateNavigator() {
//MaterialApp中不指定navigatorKey會默認初始化一個全局的NavigatorState
_navigator = widget.navigatorKey ?? GlobalObjectKey<NavigatorState>(this);
}
@override
Widget build(BuildContext context) {
//這裏會構建出一個Navigator組件,並把上面的navigatorKey寫進去,這樣就作到了Navigator的棧操做
Widget navigator;
if (_navigator != null) {
navigator = Navigator(
key: _navigator,
// If window.defaultRouteName isn't '/', we should assume it was set // intentionally via `setInitialRoute`, and should override whatever // is in [widget.initialRoute]. initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName ? WidgetsBinding.instance.window.defaultRouteName : widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName, onGenerateRoute: _onGenerateRoute, onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null ? Navigator.defaultGenerateInitialRoutes : (NavigatorState navigator, String initialRouteName) { return widget.onGenerateInitialRoutes(initialRouteName); }, onUnknownRoute: _onUnknownRoute, observers: widget.navigatorObservers, ); } } } 複製代碼
到這裏基本上能夠想到該如何實現底部購物車的功能了。
是的,咱們能夠在點單界面自定義一個Navigator
來管理搜索商品、商品詳情、商品購物車列表等路由的跳轉,其它的交由咱們MaterialApp
的Navigator
控制。
下面是功能代碼大體實現:
class OrderPage extends StatefulWidget {
@override
_OrderPageState createState() => _OrderPageState();
}
class _OrderPageState extends State<OrderPage> {
///管理點單功能Navigator的key
GlobalKey<NavigatorState> navigatorKey = GlobalKey();
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
//監聽系統返回鍵,先對自定義Navigator裏的路由作出棧處理,最後關閉OrderPage
navigatorKey.currentState.maybePop().then((value) {
if (!value) {
NavigatorUtils.goBack(context);
}
});
return Future.value(false);
},
child: Stack(
children: <Widget>[
Navigator(
key: navigatorKey,
onGenerateRoute: (settings) {
if (settings.name == '/') {
return PageRouteBuilder(
opaque: false,
pageBuilder:
(childContext, animation, secondaryAnimation) =>
//構建內容層
_buildContent(childContext),
transitionsBuilder:
(context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
transitionDuration: Duration(milliseconds: 300),
);
}
return null;
},
),
Positioned(
bottom: 0,
right: 0,
left: 0,
//購物車組件,位於底部
child: ShopCart(),
),
//添加商品進購物車的小球動畫
ThrowBallAnim(),
],
),
);
}
}
複製代碼
效果能夠看最開始的那一張GIF。
Hero
的使用很是的簡單,須要關聯的兩個組件用Hero
組件包裹,並指定相同的tag
參數,代碼以下:
///列表item
InkWell(
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Hero(
tag: widget.data,
child: LoadImage(
'${widget.data.img}',
width: 81.0,
height: 81.0,
fit: BoxFit.fitHeight,
),
),
),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => GoodsDetailsPage(data: widget.data)));
},
);
複製代碼
///詳情
Hero(
tag: tag,
child: LoadImage(
imageUrl,
width: double.infinity,
height: 300,
fit: BoxFit.cover,
),
)
複製代碼
是否是以爲這樣寫好就完事了呢,Hero的效果就會出來了?在正常狀況下是會有效果,可是在咱們這裏卻沒有任何效果,就跟普通的路由跳轉同樣樣的,這是爲啥呢?
咱們在MaterialApp
中的是有效果的,自定義的Navigator
的卻沒效果,那麼確定是MaterialApp
的Navigator
作了什麼配置。
仍是經過MaterialApp
的源碼能夠發現,在其初始化的時候會new一個HeroController
並在構造參數navigatorObservers
中添加進去
class _MaterialAppState extends State<MaterialApp> {
HeroController _heroController;
@override
void initState() {
super.initState();
_heroController = HeroController(createRectTween: _createRectTween);
_updateNavigator();
}
@override
void didUpdateWidget(MaterialApp oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.navigatorKey != oldWidget.navigatorKey) {
// If the Navigator changes, we have to create a new observer, because the
// old Navigator won't be disposed (and thus won't unregister with its
// observers) until after the new one has been created (because the
// Navigator has a GlobalKey).
_heroController = HeroController(createRectTween: _createRectTween);
}
_updateNavigator();
}
List<NavigatorObserver> _navigatorObservers;
void _updateNavigator() {
if (widget.home != null ||
widget.routes.isNotEmpty ||
widget.onGenerateRoute != null ||
widget.onUnknownRoute != null) {
_navigatorObservers = List<NavigatorObserver>.from(widget.navigatorObservers)
..add(_heroController);
} else {
_navigatorObservers = const <NavigatorObserver>[];
}
}
///....
}
複製代碼
最終是添加進WidgetsApp
構建的Navigator
構造參數observers
裏
navigator = Navigator(
key: _navigator,
// If window.defaultRouteName isn't '/', we should assume it was set
// intentionally via `setInitialRoute`, and should override whatever
// is in [widget.initialRoute].
initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName
? WidgetsBinding.instance.window.defaultRouteName
: widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName,
onGenerateRoute: _onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
? Navigator.defaultGenerateInitialRoutes
: (NavigatorState navigator, String initialRouteName) {
return widget.onGenerateInitialRoutes(initialRouteName);
},
onUnknownRoute: _onUnknownRoute,
//MaterialApp的HeroController會添加進去
observers: widget.navigatorObservers,
);
複製代碼
因此咱們只要同理在本身定義的Navigator
裏添加進去便可:
Stack(
children: <Widget>[
Navigator(
key: navigatorKey,
//自定Navigator使用不了Hero的解決方案
observers: [HeroController()],
onGenerateRoute: (settings) {
if (settings.name == '/') {
return PageRouteBuilder(
opaque: false,
pageBuilder:
(childContext, animation, secondaryAnimation) =>
_buildContent(childContext),
transitionsBuilder:
(context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
transitionDuration: Duration(milliseconds: 300),
);
}
return null;
},
),
Positioned(
bottom: 0,
right: 0,
left: 0,
child: ShopCart(),
),
//添加商品進購物車的小球動畫
ThrowBallAnim(),
],
)
複製代碼
底部購物車的灰色區域使用到了高斯模糊的效果
該效果在Flutter
中的控件是BackdropFilter
,用法以下:
BackdropFilter(
filter: ImageFilter.blur(sigmaX, sigmaY),
child: ...)
複製代碼
不過使用的時候也有小坑,若是沒有進行剪輯,那麼高斯模糊的效果會擴散至全屏,正確的寫法應該以下:
ClipRect(
BackdropFilter(
filter: ImageFilter.blur(sigmaX, sigmaY),
child: ...)
)
複製代碼
ps:其實在BackdropFilter
的源碼中有更詳細的說明,建議你們去看看
商品欄目的分類說的籠統點就是1、二級菜單對PageView
的page切換處理。
能夠把上圖右側框出的部分當作一個PageView
,左側tab
的點擊就是對PageView
進行的一個豎直方向的page切換操做,對應的tab
下沒有二級tab
的話,那麼當前page展現的就是一個ListView
。
那若是有二級tab
的話,當前page展現的是TabBar
+PageView
聯動,這個PageView
的方向是橫向水平的
若是上述的描述還不是很懂的話,不要緊,我準備了一張總的結構圖,清晰的描述了它們之間的關係:
還有一點須要注意的地方,咱們不但願每次切換tab
的時候,Widgets
都會從新加載一次,這樣對用戶的體驗是極差的,咱們要對已經加載過的page保持它的一個頁面狀態。這一點使用AutomaticKeepAliveClientMixin
能夠作到。
class SortRightPage extends StatefulWidget {
final int parentId;
final List<Sort> data;
SortRightPage(
{Key key,
this.parentId,
this.data})
: super(key: key);
@override
_SortRightPageState createState() => _SortRightPageState();
}
class _SortRightPageState extends State<SortRightPage>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
if (widget.data == null || widget.data.isEmpty) {
if (widget.parentId == -1) {
//套餐Page
return DiscountPage();
} else {
//商品列表
return SubItemPage(
key: Key('subItem${widget.parentId}'),
id: widget.parentId
);
}
} else {
//二級分類
return SubListPage(
key: Key('subList${widget.parentId}'),
data: widget.data
);
}
}
@override
bool get wantKeepAlive => true;
}
複製代碼
好了,文章到這裏七七八八的差很少了,其餘更加細節的地方你們能夠去Github上看我寫的demo,裏面對用戶交互的處理仍是蠻穩當的,但願可以幫助到你們。