在〈第一個 Flutter 專案〉中,建立 MaterialApp
實例時,可以指定 theme
特性,為 MaterialApp
的子元件們套用佈景主題。
...略
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
...略
這邊要討論的不是佈景主題本身。而是佈景主題這類資料,是作為 MaterialApp
的子元件們共用的資料,要讓這種共用資料,在子元件間傳遞,Flutter 的方式是透過 InheritedWidget
。
為了示範怎麼使用 InheritedWidget
,來利用一下〈Scaffold 與 MaterialApp〉中的 AppScaffold
,它的 build
方法中的三個 Container
,color
都是寫死的,這邊希望的是,可以藉由 ColorSuite
節點來指定顏色,ColorSuite
的定義如下:
class ColorSuite extends InheritedWidget {
Color titleColor;
Color bodyColor;
Color footerColor;
ColorSuite({Widget child, Color titleColor, Color bodyColor, Color footerColor}) : super(child: child) {
this.titleColor = titleColor == null ? Colors.blue : titleColor;
this.bodyColor = bodyColor == null ? Colors.tealAccent : bodyColor;
this.footerColor = footerColor == null ? Colors.cyan : footerColor;
}
@override
bool updateShouldNotify(ColorSuite oldWidget) {
return this.titleColor != oldWidget.titleColor ||
this.bodyColor != oldWidget.bodyColor ||
this.footerColor != oldWidget.footerColor;
}
static ColorSuite of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ColorSuite>();
}
}
你可能會想,幹嘛這麼麻煩,為何不定義為 AppScaffold
的屬性就好?這邊希望的是,被指定給 ColorSuite
的顏色資訊,會在 AppScaffold
的子樹中各階層共享,而不單只是直接子元件共享,因此 ColorSuite
繼承了 InheritedWidget
,它也是一種 Widget
:
Widget > ProxyWidget > InheritedWidget
ColorSuite
可以指定 child
與顏包,繼承 InheritedWidget
後,必須重新定義 updateShouldNotify
,決定這個 Widget
被重新建構時,是否通知子樹元件重新建構,在這邊實作的是,當 ColorSuite
重新建構時,若新舊 ColorSuite
上的顏色有不同,就通知子樹元件重新建構,像是使用者操作後更換了顏色的指定,通知子樹元件使用新的顏色重新建構。
在〈自訂 StatelessWidget〉中談過 BuildContext
,可以透過它在 Widget
樹中找尋特定的 Widget
,ColorSuite
定義了個 static
的 of
,這可以方便子元件找到其父裔中的 ColorSuite
實例,稍後就會用到。
接著就可以改造一下〈Scaffold 與 MaterialApp〉中 AppScaffold
等程式碼了,相關的說明寫在註解裡:
import 'package:flutter/material.dart';
void main() => runApp(
ColorSuite( // 建立 ColorSuite
child: AppScaffold( // 指定 AppScaffold 為子元件
title: Text(
'Flutter 筆記',
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
),
body: Text(
'內容...',
textDirection: TextDirection.ltr,
style: TextStyle(color: Colors.black)
),
footer: Text(
'版權...',
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
),
),
// 指定顏色
titleColor: Colors.green,
bodyColor: Colors.lightGreenAccent,
footerColor: Colors.teal,
),
);
// 因為實際的 App 會有狀態,AppScaffold 改繼承 StatefulWidget
// Flutter 本身的 Scaffold 也是繼承 StatefulWidget
class AppScaffold extends StatefulWidget {
final Widget title;
final Widget body;
final Widget footer;
AppScaffold({this.title, this.body, this.footer});
@override
State<AppScaffold> createState() => _AppScaffoldState();
}
class _AppScaffoldState extends State<AppScaffold> {
@override
Widget build(BuildContext context) {
// 每次重新建構時,都是找到父裔中的 ColorSuite
ColorSuite suite = ColorSuite.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
color: suite.titleColor, // 根據 ColorSuite 設定顏色
height: 100,
child: Center(child: widget.title),
),
Expanded(
child: Container(
color: suite.bodyColor, // 根據 ColorSuite 設定顏色
child: widget.body,
)
),
Container(
color: suite.footerColor, // 根據 ColorSuite 設定顏色
height: 80,
child: Center(child: widget.footer),
)
]
);
}
}
class ColorSuite extends InheritedWidget {
Color titleColor;
Color bodyColor;
Color footerColor;
ColorSuite({Widget child, Color titleColor, Color bodyColor, Color footerColor}) : super(child: child) {
this.titleColor = titleColor == null ? Colors.blue : titleColor;
this.bodyColor = bodyColor == null ? Colors.tealAccent : bodyColor;
this.footerColor = footerColor == null ? Colors.cyan : footerColor;
}
@override
bool updateShouldNotify(ColorSuite oldWidget) {
return this.titleColor != oldWidget.titleColor ||
this.bodyColor != oldWidget.bodyColor ||
this.footerColor != oldWidget.footerColor;
}
static ColorSuite of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ColorSuite>();
}
}
基本上,這就是 Flutter 中 MaterialApp
如何共享佈景主題資訊的原理,可以共享的資訊也可能是可變動的狀態,無論共享的是什麼資訊,重點在於 ColorSuite.of(context)
,也就是每次重新建構時,都是找到父裔中的 ColorSuite
,以便取得顏色資訊。
其實在〈第一個 Flutter 專案〉中,也有個類似的程式片段,也就是在 _MyHomePageState
的 build
中,Theme.of(context).textTheme.display1
就是從父裔中找到共享的佈景主題資料:
class _MyHomePageState extends State<MyHomePage> {
...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1, // 找到父裔的共享資料
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter, // 註冊 onPressed 處理器
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
方才談到,繼承 InheritedWidget
後,必須重新定義 updateShouldNotify
,決定這個 Widget
被重新建構時,是否通知子樹元件重新建構,如果子樹元件重建的代價昂貴,你可能不會想重建整個子樹,而是依條件重建子樹中的某些元件,這時可考慮繼承 InheritedModel
,之後依 aspect
的指定來決定是否重建子樹中某個部份,詳情可參考 InheritedModel
的 API 文件。