Flutter Route路由學習筆記
最近時常在使用Flutter開發APP,能達到的功能相當的多,即使沒有原生的Package,還是能夠透過其他方式來開發。
不過最近比較大的問題是在換頁的時候,常常遇到問題:切至下一頁正常,回到上一頁時,卻不是我們想要回到的頁面。
Flutter的Route是用資料結構中Stack的pop和push來設計的
pop就是回到上一頁push就是進入下一頁
聽起來很簡單,實作上卻遇到相當多的困難呢。
先備知識
在建立new screen的時候,有兩個方式只要看到是.pushNamed的,後面一定要接('/screen2')這樣的格式只要看到是.push的,也就是沒有Named的,後面一定要接(context,MaterialPageRoute(builder: (context) => Screen3(),),)的格式簡單來說:
有Named的用一般route
沒有Named的用MaterialPageRoute
必須先建立路由,確定要進入的頁面
//建立Routenew MaterialApp(
home: new Screen1(),
routes: <String, WidgetBuilder> {
'/screen1': (BuildContext context) => new Screen1(),
'/screen2' : (BuildContext context) => new Screen2(),
'/screen3' : (BuildContext context) => new Screen3(),
'/screen4' : (BuildContext context) => new Screen4()
},
)
常用方法的使用情境:
基本款
pushNamed、push、pop
例如:screen1進入screen2後,還想回到screen1
new RaisedButton(
onPressed:(){
Navigator.of(context).pushNamed('/screen2');
},
child: new Text("Push to Screen 2"),
),Navigator.of(context).pop();// 注意以下兩個功能相等(screen1->screen2,返回鍵跳回screen1)
Navigator.of(context).pushNamed("/screen2");
Navigator.of(context).push(new MaterialPageRoute(builder: (context) {return new screen2();}));
刪除「前1個」 route,push到新 route
pushReplacementNamed、popAndPushNamed、pushReplacement
進入新screen並且移除目前的screen。
例如:購物APP
為了要進行購物功能(screen3),使用者進入screen3確認了購買的商品。這時候,進入結帳畫面(screen4)。突然,使用者臨時想到還要買另一個商品(screen2),回到screen2添加至購物車後,直接再回到screen4進行結帳,省下到screen3的時間。
使用前: screen1 -> screen 2 -> screen3 -> screen4
使用後:screen1 -> screen 2 -> screen4 // 刪除了screen3// 以下三個功能相同(screen3 -> screen4,移除screen3,返回鍵進入screen2)// 進入動畫
Navigator.of(context).pushReplacementNamed('/screen4');
Navigator.of(context).pushReplacement(newRoute);//功能一樣,但是使用離開動畫
Navigator.popAndPushNamed(context, '/screen4');
刪除「先前所有」 route,push到新 route後,設為第一層
pushNamedAndRemoveUntil
例如:透過Firebase驗證登入Google帳號,進入APP主頁後,如果按下返回鍵,會跳回登入前的畫面,這還蠻嚴重的。
// 在彈出新路由之前,刪除路由棧中的所有路由
// 這樣可以保證把之前所有的路由都進行刪除,然後才push新的路由。
Navigator.of(context).pushNamedAndRemoveUntil('/Screen2', (Route<dynamic> route) => false);// Route<dynamic> route) => false確保刪除先前的所有route// 除此之外,我們還能刪除中間的screen,保留想要的screen
// 以下範例是保留screen4、screen1,刪除screen2、screen3Navigator.of(context).pushNamedAndRemoveUntil('/screen4',
ModalRoute.withName('/screen1'));// 注意以下兩個功能相等(移除所有Screen只剩下screen1)
pushNamedAndRemoveUntil(route,ModalRoute.withName('/screen1'),)
pushAndRemoveUntil(MaterialPageRoute,ModalRoute.withName('/screen1'),)
切換頁面間傳入資料(Pass)(Forward)
MaterialPageRoute
//除了MaterialPageRoute可傳資料外,一般route也可以
例如:使用Camera拍照後,要將照片傳入下個畫面。
Navigator.push(context,MaterialPageRoute(
builder: (context) => Screen3(imagePath: recordedImage.path),
),
);在Screen3中取得圖片資料class Screen3 extends StatelessWidget { final String imagePath;
Screen3(this.imagePath);
@override
Widget build(BuildContext context) {
...
child: Image.file(File(widget.imagePath),
...
}
}
切換頁面間傳回資料(Back)(Backward)
MaterialPageRoute<Datatype>
//除了MaterialPageRoute可傳資料外,一般route也可以。
Datatype可以是String, List, Map, File…..各種型態。
以下範例以傳回String為例:
new RaisedButton(onPressed: ()async{
String value = await Navigator.push(context, new MaterialPageRoute<String>(
builder: (BuildContext context) {
return new Center(
child: new GestureDetector(
child: new Text('OK'),
onTap: () { Navigator.pop(context, "Audio1"); }
),
);
}
)
);
print(value);
},
child: new Text("Return"),)
避免使用者跳出應用程式
maybePop
例如:我們的使用者在Screen1,若跳出會直接離開APP,對UX不好。所以透過maybePop()來判斷能不能跳出去。若在Screen3還能跳回Screen2;在Screen1則不行。
#20200608更新:我們也能用WillPopScope結合maybePop讓使用者選擇要不要退出APP
static Future<bool> maybePop<T extends Object>(BuildContext context, [ T result ]) {
return Navigator.of(context).maybePop<T>(result);
}@optionalTypeArgs
Future<bool> maybePop<T extends Object>([ T result ]) async {
final Route<T> route = _history.last;
assert(route._navigator == this);
final RoutePopDisposition disposition = await route.willPop();
if (disposition != RoutePopDisposition.bubble && mounted) {
if (disposition == RoutePopDisposition.pop)
pop(result);
return true;
}
return false;
}
去除AppBar上的返回按鈕
在新增Route後,AppBar通常會出現返回按鈕,如果不想讓使用者誤觸的話,可以將automaticallyImplyLeading設為false。
AppBar({
Key key,
this.leading,
this.automaticallyImplyLeading = false,
this.title,
this.actions,
this.flexibleSpace,
this.bottom,
this.elevation = 4.0,
this.backgroundColor,
this.brightness,
this.iconTheme,
this.textTheme,
this.primary = true,
this.centerTitle,
this.titleSpacing = NavigationToolbar.kMiddleSpacing,
this.toolbarOpacity = 1.0,
this.bottomOpacity = 1.0,
}) : assert(automaticallyImplyLeading != null),
assert(elevation != null),
assert(primary != null),
assert(titleSpacing != null),
assert(toolbarOpacity != null),
assert(bottomOpacity != null),
preferredSize = Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)),
super(key: key);