穿梭在不同的畫面中 - 動態名稱與資料傳送

紅寶鐵軌客
Join to follow...
Follow/Unfollow Writer: 紅寶鐵軌客
By following, you’ll receive notifications when this author publishes new articles.
Don't wait! Sign up to follow this writer.
WriterShelf is a privacy-oriented writing platform. Unleash the power of your voice. It's free!
Sign up. Join WriterShelf now! Already a member. Login to WriterShelf.
寫程式中、折磨中、享受中 ......
612   0  
·
2021/06/11
·
14 mins read


Flutter 提供了四種 Navigation & Routing 的方式:

N1
直接導航
直接給指定 screen 名稱
N2 固定名稱路由 透過路由跳到指定的 screen 名稱
N3 動態名稱路由 目的地從 onGenerateRoute() 取出  
N4 動態名稱路由 2.0 使用 State:有 Page 及 Route 兩個新的 API

前面我們已經學會「N1 直接導航」及「N2 固定名稱路由」,現在讓我們來看看:

N3 動態名稱路由:

延續我們的程式碼,它已經準備好可以用來測試動態名稱路由了。如果有人現在才加入,目前階段的程式碼請自行參考本書:程式碼備份中的 Code milestone 1

什麼是「動態名稱路由」?

其實就是要能有如下的這個網址:

www.url.com/audio/8

重點就是那個有底線的 8,它是變動的,之前介紹的方法都沒有辦法做到,「N1/直接導航」法根本就沒有路由名稱,也就是 audio 部分,「N2/固定名稱路由」法只能走固定的路由名稱, 所以,才有這第三個方法,為了支援動態的路由名稱,「N3/動態名稱路由」才出現了,哎,這一切的一切都是為了要支援網站啊。

等等,發明了那麼多方法,就是為了支援網頁?不是為了要傳那個參數「8」嗎?是的,答案是:不是,每一個方法都可以傳參數!不信我們就先來看看:


用 N1/直接導航,傳參數

先來寫接收端,我們透過 MyHomePage 來選要讀取那一筆資料,顯示在 AudioSession 上,所以 接收端是 AudioSession:

lib/screens/audio_session.dart:

class AudioSession extends StatefulWidget {
  final int arIndex;

  // do we need key?
  AudioSession({Key? key, required this.arIndex}) : super(key: key);
  //AudioSession({required this.arIndex});
  • 第 2 行:我們新增了一個接收參數 arIndex,它是整數,而且只能改一次。
  • 第 5 行:將 arIndex 列為必需的參數,一定要有這個參數才能讀資料啊,所以當然是必須的。話說這個:key,真的有必要嗎?最簡單的方法當然就是試試看,我們兩個都寫了,第 6 行沒有 key,大家可以切換試試,結果都一樣,其實在大部分的情況下,都不需要 key,除非你有兩個以上相同的 widgets 同時存在一個 screen 內,不過,話說回來,程式改來改去,現在沒有,不代表以後也沒有,所以,多寫個 key 好像不是壞事,至少不用抓錯。

再來就是要顯示出 AudioRec 的資料,如下,在同一個 audio_session.dart 內:

Card(
  child: ListTile(
    title: Text(audioRec[widget.arIndex].title),
    subtitle: Text(audioRec[widget.arIndex].description),
  ),
),
  • 第 3~4 行:顯示 AudioRec 的內容超級簡單,只要用 widget.arIndex 當 index 就好了。我們之前有大概介紹過 widget,現在在這裡又看的更清楚了,Flutter 會把 Stateful Widget 的參數及狀態透過 widget 傳給它的 State,在這裏就是剛剛傳給 AudioSession 的參數 arIndex 被藏在 widget 裡了,只要用 widget.arIndex 就可以讀出,方便又好用,有了這個,StatefulWidget 就不用再重傳一遍參數給 state 了。

就這樣,接收端就寫好了,再來就是傳送端了:

lib/screens/my_home_page.dart 中,要將 ListView 中的每一個 card 建立一個相對的跳轉:

child: ListTile(
  title: Text(audioRec[index].title),
  subtitle: Text(audioRec[index].description),
  onTap: () {
    Navigator.push(
      context, MaterialPageRoute(
        builder: (context) => AudioSession(arIndex: index),
      ),
    );
  },
  • 第 4~9 行:透過 Navigator.push,這我們之前已經學過,建立一個直接導航,唯一不同的是,這次它有帶一個 arIndex 參數了。

就這樣,傳送端也寫好了,只是我們的 myApp 報錯了,我們在 routes 中的 AudioSession 必須要有 arIndex 參數:

lib/main.dart 中:

  routes: {
    '/': (context) => MyHomePage(title: 'Happy Recorder v0.0'),
    '/audio': (context) => AudioSession(arIndex: 0),
  },

第 3 行:呼叫 AudioSession 必須要有參數,這個 route 目前只有 MyHomePage 的 FloatingActionButton 在用,它未來會是個新增動作,還沒寫,所以就先讓它傳 0 吧,也就是讓它顯示第一筆資料。

好了,現在可以試試了,讚!點選任何一個列表項目,就可以看到它相對的內容了,所以,我們證明了「N1/直接導航」是可以傳參數的!

在網頁上也動作正常,只是,它的網址不是 www.url.com/audio/8,如我們所預期,網址如下,空空如也:

結論:「N1/直接導航」是可以傳參數的,而且簡單好用,除非你一定要在瀏覽器上能顯示動態網址列,才需要使用「N3/動態名稱路由」


用 N2/固定名稱路由,走 ModalRoute 傳參數

剛剛的網址空空如也,那如果用「N2/固定名稱路由」法,就能產生我們要的網址嗎?不知道,試試就知道。

「N2/固定名稱路由」法是透過 ModalRoute 來傳參數,方法也很簡單:

  • 傳送時:使用 navigator pushNamed,將參數內含在 arguments 中;
  • 接收時:透過 ModalRoute.of( context ).settings.arguments 來接收傳來的參數。

聽無啦,講一堆不如直接上碼,延續上面的程式,接收端完全不用改,只有傳送端更改如下。

lib/screens/my_home_page.dart 中:

child: ListTile(
  title: Text(audioRec[index].title),
  subtitle: Text(audioRec[index].description),
  onTap: () {
    Navigator.pushNamed(context, '/audio', arguments: index);
  },

第 5 行:一樣要將 ListView 中的每一個 card 建立一個相對的跳轉,只是這次改呼叫 Navigator 的pushNamed,目標 screen 就是一個在 named route 中的固定名稱路由,這也都學過了,唯一新的東西就是加了一個 arguments: index,是的,我們可以透過 arguments 在「固定名稱路由」法中傳送參數。

下面的 FloatingActionButton 也要改一下,這個按鈕未來是要做新增的,目前就先把他指到 0 吧。

lib/screens/my_home_page.dart 修改位於最下面 FloatingActionButton 的 Navigator:

floatingActionButton: FloatingActionButton(
  //onPressed: _incrementCounter,
  onPressed: () {
    Navigator.pushNamed(context, '/audio', arguments: 0);
  },

第 4 行:只是新增了一個 arguments: 0 參數。

好啦,再來就是要改 route 了:

lib/main.dart 中:

routes: {
  '/': (context) => MyHomePage(title: 'Happy Recorder v0.0'),
  '/audio': (context) => AudioSession(arIndex: (ModalRoute.of(context)!.settings.arguments as int)),
},
  • 第 3 行:要傳一個 arIndex 的整數參數給 AudioSession screen,參數就在 ModalRoute.of( context )!.settings.arguments 裡,我們要的是整數,但是這是個 object,所以用 as 來做 typecast 型態轉壞,Dart 的 as 很好用,不然我們就要先用 .toString 轉成字串,再用 int.parse() 轉成整數,會多寫好多字。

我們再花一點時間來看這行:

  • ModalRoute.of(context)! 尾巴的驚嘆號是 dart 的 “Casting away nullability”,這樣寫是保證左邊的值一定不是 null,這是為了要合乎 Dart 的 null safety 寫法,在 Flutter/Dart 中,我們要很明確的告知每個物件是不是 null,當有模糊不清時,編譯器就會報錯,這就是 null safety 保護,當然,你只是「告訴」編譯器它不會是 null,如果執行時,它真的是 null 時,程式就會死給你看。 
  • ModelRoute 可以説就是 Flutter 管 Navigation 的大總管 widget,它是個大東西,我們先不用嘗試把它搞懂,現在只要知道它的 settings 是個 RouteSettings class,我們可以透過它讀出:arguments 及 name (路由名稱,如:/home) 就好。

好了,現在可以試試了,又是讚!所以固定名稱路由也可以傳參數的,在網頁上也動作正常,只是,它還是不是我們要的 www.url.com/audio/8 網址,如下,有好一點了,但是還是沒有「8」!

註:如果你要傳兩個以上的參數,可以用 Map 或是另建一個 class。


N3/動態名稱路由,用 onGenerateRoute 來傳參數

除了用 ModalRoute 以外,如果是使用 MaterialApp 或 CupertinoApp 還有另一個方法:onGenerateRoute 可以用,一樣,先來上碼,廢話少說:

lib/main.dart:

return MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  initialRoute: '/',
  routes: {
    '/': (context) => MyHomePage(title: 'Happy Recorder v0.0'),
    //'/audio': (context) => AudioSession(arIndex: (ModalRoute.of(context)!.settings.arguments as int)),
  },
  onGenerateRoute: (RouteSettings settings) {
    if (settings.name == '/audio') {
      return MaterialPageRoute(builder: (_) => AudioSession(arIndex: settings.arguments as int));
    }
    // other, goto 404 page.
    return MaterialPageRoute(builder: (_) => Page404(routeName: settings.name));
  }
);
  • 第 9 行:我們要將 /audio 改用「動態名稱路由」法,所以先把它 remark 起來。
  • 第 11 行:onGenerateRoute 這個方法出現了,後面接了一個無名函數,它的參數就是一個 settings,等等,這個 setting 好熟習啊,沒錯,它就是我們之前用的 ModelRoute 的那個 settings,它是個 RouteSettings class,所以我們可以透過它讀出:arguments 及 route 名稱。
  • 第 12~14 行:如果 route 名稱是 /audio,那就走這裡,我們又看到 MaterialPageRoute 了,我們透過它的 builder 來切換到 AudioSession screen,這樣才才會有轉場動畫。AudioSession 需要 arIndex 參數,在 onGenerateRoute 中取得參數更簡單了,直接讀 settings.arguments 就好,所以⋯⋯ 搞了半天,onGenerateRoute 根本就是一個 ModalRoute 的懶人包嘛。
  • 第 16 行:現在,我們是用程式來判斷路由名稱了,所以可以處理任何名稱,也就是管理動態名稱了,基本上,不管送來的名稱是什麼,我們都要處理,但是遇到不認識,或是不想處理的路由名稱時呢?我們通常都把它送到一個特定頁面,在網頁開發上,它有一個很有名的代號:404,網頁不存在,有人甚至把它印在衣服上,用 onGenerateRoute 可以很簡單的處理 404,就是我們現在的寫法,把它轉到一個 404 screen,我們也新增了這個 screen,如果你真的不想寫 404 screen 也很簡單,就 return null, 它就會回到 default route name,你沒改的話就是 /,也就是 root screen。

等等,你有注意到那個 (_) => func() 了嗎?那是什麼鬼啊? 其實這又是 dart 的一個簡寫,在 dart 中,我們已經知道底線是指 priavate 變數,但是它有例外,它也可以是「無用參數」的名稱,而且可以寫成 _、__、___ 也就是寫成一個、兩個或三個底線,效果跟寫成 () => func()一樣。你會說那就跟傳 null 是一樣了,沒錯,只是在 dart 中,null 是 keyword,你不能用來當參數名稱。

傳送端還是一樣,所以不用改,但是我們現在可以處理不知名的 404 路由了,所以可以先將還沒有寫好的路由放上去,反正沒有路就會到 404。

lib/screens/my_home_page.dart 

floatingActionButton: FloatingActionButton(
  //onPressed: _incrementCounter,
  onPressed: () {
    Navigator.pushNamed(context, '/new');
  },
  • 第 4 行:原來一定要到「已知」路由的,現在可以到「未知」了,MyHomePage 的這個 FloatingActionButton 本來就是要作為新增使用的,雖然我們新增的頁面及功能都還沒有寫,但是我們可以先放個 /new 路由頁面上去,找不到就會到 Page404,不會再當掉了。 

好啦,基本上這樣就改好了,啊,對了,我們還沒有寫 Page404 screen:

Android Studio 很聰明,當你新增這個程式檔案後,面對一個空白的頁面,你只要打 st 就會跳出程式樣板選項,如下,選一個你要的,就幫你寫好樣板了,404 不用會動吧,所以就用 stless = Stateless 就好了,反正以後改道 Stateful 也很方便。

套個 Scaffold,加上幾個 Widget 就像個樣了,各位也可以自己發揮想像力,404 是個工程師的玩具,很多網站的 404 都設計的很酷。

lib/screens/page_404.dart:

import 'package:flutter/material.dart';

class Page404 extends StatelessWidget {
  const Page404({Key? key, required this.routeName}) : super(key: key);

  final String? routeName;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Going to " + (routeName ?? "null") + "?"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Page Not found'),
            SizedBox(height: 5),
            Icon(
              Icons.sentiment_very_dissatisfied_outlined,
              color: Colors.redAccent,
            ),
          ],
        )
      )
    );
  }
}

這個程式內容大家應該已經很熟悉了,都已經看了好幾次了,就是一個 StatelessWidget,但是我們還是再來看一下:

  • 第 6 行:String? 是宣告這個 routeName 變數是字串,值有可能是 null;
  • 第 12 行:(routeName ?? "null") 是說如果 routeName 不是 null 就用它的值,是 null 就用 "null" 字串⋯⋯ 好像在繞口令,不過我想應該看得懂。
  • 第 19 行:SizedBox(height: 5) 是個可以設定長寬的填空 Widget,為的是要上下有個空間。

好啦,可以試試看了,很好,行動裝置與網站都如預期的運行正常,動態名稱路由也確定可以傳參數,只是,在網頁上,它完全做不到我們要的 www.url.com/audio/8 網址,連 /audio 都不見了:

同時,每個方法都一樣,網頁跳轉都無法由瀏覽器網址來操作,這樣是正常的嗎?

答案是:是的,這三種辦法都沒有辦法提供瀏覽器網址操作,也不能顯示我們所要的動態網址。


onUnknownRoute

如果不想使用 onGenerateRoute,要用「N2/固定名稱路由」法時,怎麼處理 404 呢?很簡單,就用 onUnknownRoute,如下:

routes: {
  '/': (context) => MyHomePage(title: 'Happy Recorder v0.0'),
  '/audio': (context) => AudioSession(arIndex: (ModalRoute.of(context)!.settings.arguments as int)),
},
onUnknownRoute: (RouteSettings settings) {
  return MaterialPageRoute(builder: (_) => Page404(routeName: settings.name));
},

記得要把原來 onGenerateRoute 的部份刪掉,沒刪掉雖然不會怎樣,程式反正根本不會走到下面了,但是未來要回頭改程式時,一定會很亂。

 

傳回參數

我們也可以用 Navigator.pop 傳回參數,只要用 Pop 夾帶:

Navigator.pop(context, "傳回這字串");

在 push 端用個變數接收:

final _backMsg = await Navigator.pushNamed(context, '/abc', ...);

很簡單吧,_backMsg 就會得到 "傳回這字串" 了!


Flutter on Web 的強項

是的,Flutter 好像不能用來取代其他的網頁開發平台 ,但是,我們要回歸需求的原點,Flutter 是跨平台的開發環境,它的 Web 網頁本質上是要提供跟「行動裝置」一樣的使用者體驗,它並不適合用來開發一般傳統的 HTML 網站,是的,它也許不適合用來架設 blog 網站,但是下面的幾種新一代的網站技術,卻又是 Flutter on Web 的強項

  • Progressive Web Application:漸進式網路應用程式,一般的定義是:可以離線運作,使用網頁技術(JavaScript、HTML、CSS⋯⋯),可以使用網頁推播。
  • Single Page Application:單頁應用網站
  • Existing mobile applications:將行動 App 搬到 Web 上。

所以,Flutter 做不到我們要的 www.url.com/audio/8 網址嗎?答案是:做得到,只是會很麻煩,要用 Flutter 的「N4/動態名稱路由 2.0,Navigator 2.0」,或是使用外掛,如:Fluro

動態名稱路由 2.0,也就是 Navigator 2.0,會使用到 App 狀態 state, 所以我們會等到介紹完 state 後,再回來談它了。


Bouns ~ Hero 動畫初體驗!

在結束前,還要分享一個很酷又簡單的導航功能,Flutter 的導航有提供一個非常簡便的動畫轉場方法,the HERO!

行動裝置的使用者體驗,也就是大家常掛在嘴上的 UX,有很多是透過動畫來達成的,Flutter 當然支援動畫,只是要使用它並不容易,我們還沒學到,也還不會用,不過Flutter 導航有一個很簡單好用的 Hero widget,只要兩行就把動畫帶上線了!

要使用 Hero Widget 超級簡單,只要把「起點」跟「終點」的 Widget 用 Hero() 包起來就好,所以在我們目前的程式中,「起點」就是:

lib/screens/my_home_page.dart:

body: ListView.builder(
  itemCount: audioRec.length,
  itemBuilder: (context, index) {
    return Hero(
      tag: 'audio_rec_$index',
      child: Card(
  • 第 4~5 行:我們的起點是 MyHomePage ListView 中的 Card,所以把它用 Hero 包起來,Hero 一定要有一個 unique 的 tag,unique 就是獨一無二的,所以我們就創造了一個 audio_rec_$index 字串,也就說,如果是第三筆資料,那就是 audio_rec_3。

「起點」有了,再來就是「終點」:

lib/screens/audio_session.dart:

Column(
  children: [
    Hero(
      tag: 'audio_rec_${widget.arIndex}',
      child: Card(
        child: ListTile(
          title: Text(audioRec[widget.arIndex].title),
          subtitle: Text(audioRec[widget.arIndex].description),
        ),
      ),
    ),

「終點」widget 不能亂選,做好是跟「起點」的 widget 樹是一樣的,在我們的程式裡,剛好「終點」有個 Card widget,裡面也幾乎跟「起點」的 widget 一樣,用動畫連接這兩個 widget 也很傳神的表達了 UX 中的連接,所以我們就:

  • 第 3~4 行:我們的起點是 AudioSession 裡的 Card,所以也要把它用 Hero 包起來,它的 unique tag 是 audio_rec_${widget.arIndex} 字串,變數 exp 在字串中要用 {} 包起來,這樣就跟起點是一樣的了,如果是第三筆資料,也會是 audio_rec_3,透過 unique tag,我們的動畫就連接起來了。

趕快來試試,哈哈哈,很好玩,而且各位一定要把 devTools 中的慢動作打開,這樣就可以看到 Flutter 是怎麼自動將你的 Hero Widget 用動畫連接了。 

Hero 基本上只能動畫連接相同的 Widget,各位如果不信邪,可以把「終點」換一個大一點的 Widget,很快你就會看到類似 RenderFlex overflowed by xx pixels 的錯誤,也就是告訴你說,你的圖太大了,動畫越界了,好啦,我們只是先體驗一下 Flutter 的動畫,以後的章節會有詳細的用法與說明。

好啦,我們現在知道怎麼在 Flutter 的 navigator 中傳送資料了。


最後的程式碼,請自行參考本書:程式碼備份中的 Code milestone 2



WriterShelf™ is a unique multiple pen name blogging and forum platform. Protect relationships and your privacy. Take your writing in new directions. ** Join WriterShelf**
WriterShelf™ is an open writing platform. The views, information and opinions in this article are those of the author.


Article info

This article is part of:
Categories:
Tags:
Date:
Published: 2021/06/11 - Updated: 2021/09/18
Total: 4168 words


Share this article:
About the Author

很久以前就是個「寫程式的」,其實,什麼程式都不熟⋯⋯
就,這會一點點,那會一點點⋯⋯




Join the discussion now!
Don't wait! Sign up to join the discussion.
WriterShelf is a privacy-oriented writing platform. Unleash the power of your voice. It's free!
Sign up. Join WriterShelf now! Already a member. Login to WriterShelf.