本文首先講的Flutter中的路由,然後主要講下Flutter中棧管理的幾種方法。
- 瞭解下Route和Navigator
- 簡單的路由
- 命名路由
- 自定義路由
- Flutter中使用的路由場景
- Flutter中的路由棧管理
- 實戰
- 程式碼連結
- 下一步
瞭解下Route和Navigator
在Flutter中,我們需要在不同螢幕或者頁面之間進行切換和傳送資料,這些“screens”或者“pages”被稱為Route(路由),是由一個Navigator的小部件進行管理。
Navigator可以管理包含若干Route物件的堆疊,並提供了管理的方法,平常我們經常用的就是[Navigator.push]和[Navigator.pop]。
儘管我們可以自己直接建立一個navigator,但是當我們建立一個WidgetsApp或者MaterialApp,Flutter會自動預設建立一個Navigator。 所以我們一般是使用由[WidgetsApp]或者[MaterialApp]所建立的Navigator就行了,然後通過呼叫[Navigator.of] 來拿到當前的Navigator的狀態NavigatorState,然後呼叫它的pop或者push方法。
簡單的路由
比如要導航到一個新的頁面,我們可以建立一個[MaterialPageRoute]的例項,然後呼叫Navigator.of(context).push()方法就將新頁面新增到堆疊的頂部。
返回上一個頁面,則呼叫Navigator.pop(context)就可以從堆疊中刪除這個螢幕;
Navigator.of(context)
.push(new MaterialPageRoute(builder: (context) {
return new DetailPage();
}));
Navigator.pop(context);
複製程式碼
命名路由
如果每次跳轉到一個新的路由頁面,都要跟上面一樣的寫法,建立MaterialPageRoute例項然後呼叫push方法,這樣的話就太麻煩了。
所以,Flutter提供了另外一種方式來管理路由,可以使用命名路由,然後使用Navigator.pushNamed()方法來彈出路由。
建立MaterialApp的時候需要傳入一個routes引數,routes本質上是一個Map<String,WidgetBuilder>,key值對應自定義的路徑名字,value值會對映到對應的WidgetBuilder,我們可以在WidgetBuilder中建立對應的頁面。
Navigator.pushNamed()方法有兩個引數(BuildContext,String),第一個是上下文,第二個是在路由中預定義的string。
特殊情況處理:當push一個不存在的路由頁面的時候,需要進行提示操作。可以使用UnknownRoute的屬性。比如下面的例子, 當push一個不存在的路由的時候,會跳轉到NotFoundPage的頁面。
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
routes: {
//Map<String, WidgetBuilder>
"/splash": (context) => new SplashPage(),
"/login": (context) => new LoginPage(),
"/home": (context) => new HomePage(),
"/detail": (context) => new DetailPage(),
},
onUnknownRoute: (RouteSettings setting) {
String name = setting.name;
print("onUnknownRoute:$name");
return new MaterialPageRoute(builder: (context) {
return new NotFoundPage();
});
},
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SplashPage(),
);
}
}
//彈出路由,跳轉到其他頁面
Navigator.of(context).pushNamed("/detail");
複製程式碼
自定義路由
比如我們上面使用的是MaterialPageRoute,在頁面切換的時候,會有預設的自適應平臺的過渡動畫。 如果想自定義頁面的進場和出場動畫,那麼需要使用PageRouteBuilder來建立路由。 PageRouteBuilder是主要的部分,一個是“pageBuilder”,用來建立所要跳轉到的頁面,另一個是“transitionsBuilder”,也就是我們可以自定義的轉場效果。
PageRouteBuilder({
RouteSettings settings,
@required this.pageBuilder,//構造頁面
this.transitionsBuilder = _defaultTransitionsBuilder,//建立轉場動畫
this.transitionDuration = const Duration(milliseconds: 300),//轉場動畫的持續時間
this.opaque = true,//是否是透明的
this.barrierDismissible = false,//舉個例子,比如AlertDialog也是利用PageRouteBuilder進行建立的,barrierDismissible若為false,點選對話方塊周圍,對話方塊不會關閉;若為true,點選對話方塊周圍,對話方塊自動關閉。
this.barrierColor,
this.barrierLabel,
this.maintainState = true,
}
複製程式碼
只修改單獨一個頁面的過渡動畫,可以這樣操作,例如下面的程式碼,
// 自定義跳轉動畫
Navigator.push(
context,
PageRouteBuilder(
opaque: false,
pageBuilder: (BuildContext context, _, __) {
return new HomePage();
},
transitionsBuilder: (___, Animation<double> animation,
____, Widget child) {
return FadeTransition(
opacity: animation,
child: RotationTransition(
turns: Tween<double>(begin: 0.5, end: 1.0)
.animate(animation),
child: child,
),
);
}));
複製程式碼
Flutter中使用的路由場景
在Flutter中,我們會使用到這些方法,例如[showDialog()], [showMenu()], and [showModalBottomSheet()]等,這些方法其實本質上是建立了一個路由的頁面後,並呼叫Navigator的push方法去push到當前的螢幕上。
showDialog()其實是呼叫了showGeneralDialog(),所以下面貼了showGeneralDialog的原始碼,可以看出,也是利用了Navigator的push方法的。
這裡插一下,關於對話方塊的使用,比如列表對話方塊,自定義對話方塊的使用和踩坑,可以看下我的另外一篇文章:Flutter之Dialog使用和踩坑
Future<T> showGeneralDialog<T>({
@required BuildContext context,
@required RoutePageBuilder pageBuilder,
bool barrierDismissible,
String barrierLabel,
Color barrierColor,
Duration transitionDuration,
RouteTransitionsBuilder transitionBuilder,
}) {
return Navigator.of(context, rootNavigator: true).push<T>(_DialogRoute<T>(
pageBuilder: pageBuilder,
barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel,
barrierColor: barrierColor,
transitionDuration: transitionDuration,
transitionBuilder: transitionBuilder,
));
}
複製程式碼
Flutter中的路由棧管理
相信大家對棧Stack都有一定的瞭解,push方法是將元素新增到堆疊的頂部,pop方法是刪除頂部元素。
下面用圖文的方式來講解Flutter中幾個管理棧的方法之間的區別。
push()
push(),就是直接將一個元素插入到堆疊的頂部。
這個方法很簡單,並且我們會經常用到。比如從Screen1中利用navigator的push方法,將Screen2的路由彈到堆疊的頂部。堆疊的情況如下圖:
可以利用下面的兩個push方法,實現這個目的。
Navigator.of(context).pushNamed("/111");
Navigator.of(context).push(route);
複製程式碼
pop()
pop(), 就是將堆疊的頂部元素進行刪除,回退到上一個介面。
比如上面的例子,在Screen2中利用pop()將頂部的Screen2從堆疊中移除,之後的堆疊如下圖:
Navigator.of(context).pop();
複製程式碼
注意:
- 當利用push()跳轉到一個使用Scaffold的頁面的時候,Scaffold會自動在其AppBar上新增一個“ 後退 ”按鈕,點選按鈕會自動呼叫Navigator.pop()。
- 在Android中,按下裝置後退按鈕也會自動呼叫Navigator.pop()。
pushReplacementNamed()和popAndPushNamed()
有下面的這種場景,我們進入到Screen3頁面後,要跳轉到Screen4頁面,不過點選返回按鈕,並不想回退到Screen3頁面。也就是想將Screen4的元素插入棧頂的同時,將Screen3的元素夜進行移除。
這個時候,我們就要用到pushReplacementNamed()或者popAndPushNamed(),pushReplacement()都可以實現這個目的。
Navigator.of(context).pushReplacementNamed('/screen4');
Navigator.popAndPushNamed(context, '/screen4');
Navigator.of(context).pushReplacement(newRoute);
複製程式碼
pushNamedAndRemoveUntil()
一般會有這種場景,我們在已經登入的情況下,在設定介面會有個退出使用者登入的按鈕,點選後會登出使用者退出登入,並且會跳轉到登入介面。那麼路由棧的變化應該會如下圖所示:
如果只是簡單的進行push一個LoginScreen的操作的話,那麼按返回鍵的話,會回到上一個頁面,這樣的邏輯是不對的。
所以我們應該刪除掉路由棧中的所有route,然後再彈出LoginScreen。這個時候就要用到pushNamedAndRemoveUntil()或者pushAndRemoveUntil()了。
typedef RoutePredicate = bool Function(Route<dynamic> route);
//第一個引數context是上下文的context,
//第二個引數newRouteName是新的路由所命名的路徑
//第三個引數predicate,返回值是bool型別,按照我的理解,就是用來判斷Until所結
//束的時機,如果為false的話,就會一直繼續執行Remove的操作,直到為true的時候,停止Remove操作,然後才執行push操作。
static Future<T> pushNamedAndRemoveUntil<T extends Object>(BuildContext context, String newRouteName, RoutePredicate predicate) {
return Navigator.of(context).pushNamedAndRemoveUntil<T>(newRouteName, predicate);
}
複製程式碼
- 如果想在彈出新路由之前,刪除路由棧中的所有路由,那可以使用下面的這種寫法,(Route route) => false,這樣能保證把之前所有的路由都進行刪除,然後才push新的路由。
Navigator.of(context).pushNamedAndRemoveUntil('/LoginScreen', (Route<dynamic> route) => false);
複製程式碼
- 如果想在彈出新路由之前,刪除路由棧中的部分路由。比如只彈出Screen1路由上面的Screen3和Screen2,然後再push新的Screen4,堆疊的情況如下圖:
利用ModalRoute.withName(name),來執行判斷,可以看下面的原始碼,當所傳的name跟堆疊中的路由所定義的時候,會返回true值,不匹配的話,則返回false。
Navigator.of(context).pushNamedAndRemoveUntil('/screen4',ModalRoute.withName('/screen1'));
//ModalRoute.withName的原始碼
static RoutePredicate withName(String name) {
return (Route<dynamic> route) {
return !route.willHandlePopInternally
&& route is ModalRoute
&& route.settings.name == name;
};
}
複製程式碼
popUntil()
popUntil()方法的過程其實跟上面差不多,就是是少了push一個新頁面的操作,只是單純的進行移除路由操作。
popUntil(RoutePredicate predicate);
Navigator.of(context).popUntil(ModalRoute.withName("/XXX"));
複製程式碼
實戰
這裡寫了一個Demo,將上面的幾種管理棧的用法都運用了一下。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
routes: {
//Map<String, WidgetBuilder>
"/splash": (context) => new SplashPage(),
"/login": (context) => new LoginPage(),
"/home": (context) => new HomePage(),
"/detail": (context) => new DetailPage(),
},
onUnknownRoute: (RouteSettings setting) {
String name = setting.name;
print("onUnknownRoute:$name");
return new MaterialPageRoute(builder: (context) {
return new NotFoundPage();
});
},
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SplashPage(),
);
}
}
複製程式碼
首先是定義在MaterialApp中定義多個路由,Demo中有多個路由:
- SplashPage 啟動頁面,設定2秒後自動跳轉到LoginPage
- LoginPage 登入頁面,點選按鈕,模仿登入成功的動作並跳轉到HomePage
- HomePage 主頁,顯示一個列表,點選列表項跳轉到DetailPage
- DetailPage 詳細資訊頁面,並且有一個退出登入的按鈕
- NotFoundPage 當沒有路由可以匹配的時候,彈出這個頁面
下面簡單講一下程式碼:
SplashPage->LoginPage,LoginPage->HomePage
用的都是pushReplacementNamed()。因為跳到登入介面後,不需要返回到SplashPage,所以需要將SplashPage從路由棧中移除。LoginPage->HomePage,也是同樣的道理。
Navigator.of(context).pushReplacementNamed("/login");
Navigator.of(context).pushReplacementNamed("/home");
複製程式碼
HomePage->DetailPage
使用的是簡單的pushNamed()就可以了,沒必要移除HomePage,因為從DetailPage點選返回後,需要返回到HomePage介面。
//下面的兩種寫法都是可以的
Navigator.of(context).pushNamed("/detail");
Navigator.of(context)
.push(new MaterialPageRoute(builder: (context) {
return new DetailPage();
}));
複製程式碼
DetailPage->LoginPage
利用的是從DetailPage點選退出登入按鈕,會彈出路由棧中的所有路由頁面,然後再將LoginPage的路由插入到棧頂。這樣的話,路由棧中就只剩下LoginPage了,若是點選返回按鈕的話,預設就會退出應用程式了,因為堆疊為空了。
Navigator.of(context).pushNamedAndRemoveUntil("/login", (Route<dynamic> route) => false);
複製程式碼
程式碼連結
歡迎大家關注我的公眾號,會推送關於Flutter和Android學習的一些文章
下一步
學習使用fluro的第三方路由框架,並做下整理和總結。