天天看點

layui點選左側導航欄在右側body顯示_Flutter路由導航

layui點選左側導航欄在右側body顯示_Flutter路由導航

我們通常會用屏(Screen)來稱呼一個頁面(Page),一個完整的App應該是有多個Page組成的。

在之前的案例(豆瓣)中,我們通過IndexedStack來管理了首頁中的Page切換:

首頁-書影音-小組-市集-我的

通過點選BottomNavigationBarItem來設定IndexedStack的index屬性來切換

除了上面這種管理頁面的方式,我們還需要實作其它功能的頁面跳轉:比如點選一個商品跳轉到詳情頁,某個按鈕跳轉到發送朋友圈、微網誌的編輯頁面。

這種頁面的管理和導航,我們通常會使用路由進行統一管理。

一. 路由管理

1.1. 認識Flutter路由

路由的概念由來已久,包括

網絡路由

後端路由

,到現在廣為流行的

前端路由

  • 無論路由的概念如何應用,它的核心是一個

    路由映射表

  • 比如:名字

    detail

    映射到

    DetailPage

    頁面等
  • 有了這個映射表之後,我們就可以友善的根據名字來完成路由的轉發(在前端表現出來的就是頁面跳轉)

在Flutter中,路由管理主要有兩個類:Route和Navigator

1.2. Route

Route:一個頁面要想被路由統一管理,必須包裝為一個Route

  • 官方的說法很清晰:An abstraction for an entry managed by a Navigator .

但是Route是一個抽象類,是以它是不能執行個體化的

  • 在上面有一段注釋,讓我們檢視MaterialPageRoute來使用
/// See [MaterialPageRoute] for a route that replaces the
/// entire screen with a platform-adaptive transition.
abstract class Route<T> {
}
           

事實上MaterialPageRoute并不是Route的直接子類:

  • MaterialPageRoute在不同的平台有不同的表現
  • 對Android平台,打開一個頁面會從螢幕底部滑動到螢幕的頂部,關閉頁面時從頂部滑動到底部消失
  • 對iOS平台,打開一個頁面會從螢幕右側滑動到螢幕的左側,關閉頁面時從左側滑動到右側消失
  • 當然,iOS平台我們也可以使用CupertinoPageRoute
MaterialPageRoute -> PageRoute -> ModalRoute -> TransitionRoute -> OverlayRoute -> Route
           

1.3. Navigator

Navigator:管理所有的Route的Widget,通過一個Stack來進行管理的

  • 官方的說法也很清晰:A widget that manages a set of child widgets with a stack discipline.

那麼我們開發中需要手動去常見一個Navigator嗎?

  • 并不需要,我們開發中使用的MaterialApp、CupertinoApp、WidgetsApp它們預設是有插入Navigator的
  • 是以,我們在需要的時候,隻需要直接使用即可
Navigator.of(context)
           

Navigator有幾個最常見的方法:

// 路由跳轉:傳入一個路由對象
Future<T> push<T extends Object>(Route<T> route)

// 路由跳轉:傳入一個名稱(命名路由)
Future<T> pushNamed<T extends Object>(
  String routeName, {
    Object arguments,
  })

// 路由傳回:可以傳入一個參數
bool pop<T extends Object>([ T result ])
           

二. 路由基本使用

1.1. 基本跳轉

我們來實作一個最基本跳轉:

  • 建立首頁頁面,中間添加一個按鈕,點選按鈕跳轉到詳情頁面
  • 建立詳情頁面,中間添加一個按鈕,點選按鈕傳回到首頁頁面
layui點選左側導航欄在右側body顯示_Flutter路由導航

核心的跳轉代碼如下(首頁中代碼):

// RaisedButton代碼(隻貼出核心代碼)
RaisedButton(
  child: Text("打開詳情頁"),
  onPressed: () => _onPushTap(context),
),

// 按鈕點選執行的代碼
_onPushTap(BuildContext context) {
  Navigator.of(context).push(MaterialPageRoute(
    builder: (ctx) {
      return DetailPage();
    }
  ));
}
           

核心的傳回代碼如下(詳情頁中代碼):

// RaisedButton代碼(隻貼出核心代碼)
RaisedButton(
  child: Text("傳回首頁"),
  onPressed: () => _onBackTap(context),
)
// 按鈕點選執行的代碼
_onBackTap(BuildContext context) {
  Navigator.of(context).pop();
}
           

1.2. 參數傳遞

在跳轉過程中,我們通常可能會攜帶一些參數,比如

  • 首頁跳到詳情頁,攜帶一條資訊:a home message
  • 詳情頁傳回首頁,攜帶一條資訊:a detail message
layui點選左側導航欄在右側body顯示_Flutter路由導航

首頁跳轉核心代碼:

  • 在頁面跳轉時,會傳回一個Future
  • 該Future會在詳情頁面調用pop時,回調對應的then函數,并且會攜帶結果
_onPushTap(BuildContext context) {
  // 1.跳轉代碼
  final future = Navigator.of(context).push(MaterialPageRoute(
    builder: (ctx) {
      return DetailPage("a home message");
    }
  ));

  // 2.擷取結果
  future.then((res) {
    setState(() {
      _message = res;
    });
  });
}
           

詳情頁傳回核心代碼:

_onBackTap(BuildContext context) {
  Navigator.of(context).pop("a detail message");
}
           

1.3. 傳回細節

但是這裡有一個問題,如果使用者是點選右上角的傳回按鈕,如何監聽呢?

方法一:自定義傳回的按鈕(在詳情頁中修改Scaffold的appBar)

appBar: AppBar(
  title: Text("詳情頁"),
  leading: IconButton(
    icon: Icon(Icons.arrow_back),
    onPressed: () {
      Navigator.of(context).pop("a back detail message");
    },
  ),
),
           

方法二:監聽傳回按鈕的點選(給Scaffold包裹一個WillPopScope)

  • WillPopScope有一個onWillPop的回調函數,當我們點選傳回按鈕時會執行
  • 這個函數要求有一個Future的傳回值:
    • true:那麼系統會自動幫我們執行pop操作
    • false:系統不再執行pop操作,需要我們自己來執行
return WillPopScope(
  onWillPop: () {
    Navigator.of(context).pop("a back detail message");
    return Future.value(false);
  },
  child: Scaffold(
    appBar: AppBar(
      title: Text("詳情頁"),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          RaisedButton(
            child: Text("傳回首頁"),
            onPressed: () => _onBackTap(context),
          ),
          Text(_message, style: TextStyle(fontSize: 20, color: Colors.red),)
        ],
      ),
    ),
  ),
);
           

三. 命名路由使用

3.1. 基本跳轉

我們可以通過建立一個新的Route,使用Navigator來導航到一個新的頁面,但是如果在應用中很多地方都需要導航到同一個頁面(比如在開發中,首頁、推薦、分類頁都可能會跳到詳情頁),那麼就會存在很多重複的代碼。

在這種情況下,我們可以使用

命名路由(named route)
  • 命名路由是将名字和路由的映射關系,在一個地方進行統一的管理
  • 有了命名路由,我們可以通過

    Navigator.pushNamed()

    方法來跳轉到新的頁面

命名路由在哪裡管理呢?可以放在MaterialApp的

initialRoute

routes

  • initialRoute

    :設定應用程式從哪一個路由開始啟動,設定了該屬性,就不需要再設定

    home

    屬性了
  • routes

    :定義名稱和路由之間的映射關系,類型為Map<String, WidgetBuilder>

修改MaterialApp中的代碼:

return MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue, splashColor: Colors.transparent
  ),
  initialRoute: "/",
  routes: {
    "/home": (ctx) => HYHomePage(),
    "/detail": (ctx) => HYDetailPage()
  },
);
           

修改跳轉的代碼:

_onPushTap(BuildContext context) {
  Navigator.of(context).pushNamed("/detail");
}
           

在開發中,為了讓每個頁面對應的routeName統一,我們通常會在每個頁面中定義一個路由的常量來使用:

class HYHomePage extends StatefulWidget {
  static const String routeName = "/home";
}

class HYDetailPage extends StatelessWidget {
  static const String routeName = "/detail";
}
           

修改MaterialApp中routes的key

initialRoute: HYHomePage.routeName,
routes: {
  HYHomePage.routeName: (ctx) => HYHomePage(),
  HYDetailPage.routeName: (ctx) => HYDetailPage()
},
           

3.2. 參數傳遞

因為通常命名路由,我們會在定義路由時,直接建立好對象,比如HYDetailPage()

那麼,命名路由如果有參數需要傳遞呢?

pushNamed時,如何傳遞參數:

_onPushTap(BuildContext context) {
  Navigator.of(context).pushNamed(HYDetailPage.routeName, arguments: "a home message of naned route");
}
           

在HYDetailPage中,如何擷取到參數呢?

  • 在build方法中ModalRoute.of(context)可以擷取到傳遞的參數
Widget build(BuildContext context) {
    // 1.擷取資料
    final message = ModalRoute.of(context).settings.arguments;
  }
           

3.3. 路由鈎子

3.3.1. onGenerateRoute

加入我們有一個HYAboutPage,也希望在跳轉時,傳入對應的參數message,并且已經有一個對應的構造方法

在HYHomePage中添加跳轉的代碼:

RaisedButton(
  child: Text("打開關于頁"),
  onPressed: () {
    Navigator.of(context).pushNamed(HYAboutPage.routeName, arguments: "a home message");
  },
)
           

HYAboutPage的代碼:

class HYAboutPage extends StatelessWidget {
  static const String routeName = "/about";
  final String message;

  HYAboutPage(this.message);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("關于頁面"),
      ),
      body: Center(
        child: Text(message, style: TextStyle(fontSize: 30, color: Colors.red),),
      ),
    );
  }
}
           

但是我們繼續使用routes中的映射關系,就不好進行配置了,因為HYAboutPage必須要求傳入一個參數;

這個時候我們可以使用onGenerateRoute的鈎子函數:

  • 當我們通過pushNamed進行跳轉,但是對應的name沒有在routes中有映射關系,那麼就會執行onGenerateRoute鈎子函數;
  • 我們可以在該函數中,手動建立對應的Route進行傳回;
  • 該函數有一個參數RouteSettings,該類有兩個常用的屬性:
    • name: 跳轉的路徑名稱
    • arguments:跳轉時攜帶的參數
onGenerateRoute: (settings) {
  if (settings.name == "/about") {
    return MaterialPageRoute(
      builder: (ctx) {
        return HYAboutPage(settings.arguments);
      }
    );
  }
  return null;
},
           
layui點選左側導航欄在右側body顯示_Flutter路由導航

3.3.2. onUnknownRoute

如果我們打開的一個路由名稱是根本不存在,這個時候我們希望跳轉到一個統一的錯誤頁面。

比如下面的abc是不存在有對應的頁面的

  • 如果沒有進行特殊的處理,那麼Flutter會報錯。
RaisedButton(
  child: Text("打開未知頁面"),
  onPressed: () {
    Navigator.of(context).pushNamed("/abc");
  },
)
           

我們可以建立一個錯誤的頁面:

class UnknownPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("錯誤頁面"),
      ),
      body: Container(
        child: Center(
          child: Text("頁面跳轉錯誤"),
        ),
      ),
    );
  }
}
           

并且設定onUnknownRoute

onUnknownRoute: (settings) {
  return MaterialPageRoute(
    builder: (ctx) {
      return UnknownPage();
    }
  );
},
           
備注:所有内容首發于公衆号,之後除了Flutter也會更新其他技術文章,TypeScript、React、Node、uniapp、mpvue、資料結構與算法等等,也會更新一些自己的學習心得等,歡迎大家關注
layui點選左側導航欄在右側body顯示_Flutter路由導航

繼續閱讀