Flutter Route路由學習筆記

Yanwei Liu
11 min readJun 7, 2020

--

最近時常在使用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、screen3
Navigator.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);

參考資料:

--

--

No responses yet