Navigator 與 MaterialPageRoute


在〈Navigator 與 Route〉中,簡介了 NavigatorRoute 的關係,必須記得的是,Route 不等於頁面,Route 代表的是資源的銜接,頁面只是資源的一部份,有了這個正確觀念後,接下來討論頁面間的資料傳遞才有意義。

MaterialPageRoute 為例,如何將資料傳給接下來要呈現的頁面呢?例如,傳給〈Navigator 與 Route〉中範例的 DetailPage?建構 MaterialPageRoute 時,指定的 builder 就告訴你答案了:

...略
  Navigator.push(context,
    MaterialPageRoute(builder: (_) => DetailPage())
  );

...略

也就是透過 DetailPage 的建構式;從 DetailPage 回到 MainPage 時呢?Navigatorpush 其實是個非同步方法,它傳回的是 Future 實例,RouteNavigator 管理的堆疊中彈出時,也就是 Navigator.pop 執行時,若指定了第二個 result 參數的值,該值就會成為 push 傳回的 Future 實例之結果。

MaterialPageRoute 其實支援泛型,如果預期 MaterialPageRouteNavigator 管理的堆疊中彈出時,也就是 Navigator.pop 執行時,第二個 result 參數值的型態是字串,就可以如下撰寫:

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
  title: 'Openhome',
  home: MainPage(),
));

class MainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('主畫面'),
      ),
      body: GestureDetector(
        // 註冊一個非同步處理器
        onTap: () async {
          // 等待頁面的結果
          String result = await Navigator.push(context,
            // 傳遞給頁面的資料會作為建構 DetailPage 的值
            MaterialPageRoute(builder: (_) => DetailPage('說明'))
          );
          print(result);
        },
        child: Image.asset('images/caterpillar.png'),
      ),
    );
  }
}

class DetailPage extends StatelessWidget {
  String title;

  DetailPage(this.title);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: GestureDetector(
        // 指定 pop 的第二個參數值
        onTap: () => Navigator.pop(context, '結果值'),
        child: Center(
          child: Image.asset('images/caterpillar.png'),
        ),
      ),
    );
  }
}

執行結果如下:

Navigator 與 MaterialPageRoute

那麼問題來了,如果 DetailPage 頁面必須擁有狀態呢?繼承 StatefulWidget?對於操作 DetailPage 期間產生的狀態變化,確實是該這麼做,問題在於從 MainPage 切換為 DetailPage 時,繼承了 StatefulWidgetDetailPage,每次都會產生新的狀態物件,先前操作的狀態就無法保留了。

若要避免複雜的狀態管理,最簡單的方式,當然就是不考慮之前頁面的操作狀態了,每次切換頁面就視為新的操作流程。

若要真的想接續狀態,是將 DetailPage 的狀態儲存下來,下次切換至 DetailPage,將對應狀態傳給 DetailPage 進行建構,儲存的地方可以是檔案、遠端,或者是放在主頁面,例如:

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
  title: 'Openhome',
  home: MainPage(),
));

class MainPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState()  => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  // 在主頁面保存次頁面切回時的狀態
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('主畫面'),
      ),
      body: GestureDetector(
        onTap: () async {
          // 在主頁面保存次頁面切回時的狀態
          _counter = await Navigator.push(context,
              // 傳入上次保有的狀態
              MaterialPageRoute(builder: (_) => ClickPage(_counter, title: '按我'))
          );
        },
        child: Image.asset('images/caterpillar.png'),
      ),
    );
  }
}

class ClickPage extends StatefulWidget {
  ClickPage(this._counter, {Key key, this.title}) : super(key: key);

  // 次頁面自己目前的操作狀態   
  final int _counter;
  final String title;

  @override
  _ClickPageState createState() => _ClickPageState(_counter);
}

class _ClickPageState extends State<ClickPage> {
  int _counter;

  _ClickPageState(this._counter);

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        leading: new IconButton(
          icon: new Icon(Icons.arrow_back),
          // 按功能列的箭號按鈕時,傳回 _counter
          onPressed: () => Navigator.pop(context, _counter),
        ),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              '你按下按鈕的次數:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

這個範例的 ClickPage 是修改自〈第一個 Flutter 專案〉,要注意的是,AppBar 上預設的箭號按鈕按下時,沒有預設的處理器,因此必須自行指定,這可透過 leading 來建立新的 IconButton 來設定。

以下是操作的結果:

Navigator 與 MaterialPageRoute