University of London / MSc Computer Science: Project(13〜24 週目)
October 3, 2024
ロンドン大学で MSc Computer Science: Project モジュールを履修中。
講義内容に関して記録した個人的なスタディノートです。
全 24 週のうち 13〜24 週目の内容を記録します。(13 週目開始:2024 年 7 月 8 日 / 12 週目終了:2024 年 10 月 2 日)
...ロンドン大学で MSc Computer Science: Project モジュールを履修中。
講義内容に関して記録した個人的なスタディノートです。
全 24 週のうち 13〜24 週目の内容を記録します。(13 週目開始:2024 年 7 月 8 日 / 12 週目終了:2024 年 10 月 2 日)
...自分のブログを検索してみると、3年ほど前にもドメインレジストラを引っ越そうという記事を書いていました。
その時はお名前.com を使っていたらしく、引越し先を検討していた様子。結局お名前.com を使い続けるという終わりになっていたのだけれども、実際にはその後しばらくして Google Domains に引っ越しました。Google Domains は UI が極めてシンプルで使いやすく、また DNS 設定変更後の設定浸透がなぜか非常に早く、大満足していました。(たしか、料金は特に安いということはなかったけれども。)
...以前に VMware Fusion Player で Mac に Ubuntu 環境を構築したことがありますが、今回は Canonical Multipass で Ubuntu 環境を作ってみます。
以下のページで説明されています。非常にシンプルです。
...次のような Flutter アプリがあったとします。アプリでは myConstant という定数を使用しています。この定数は将来的に変更される可能性がありますが、その場合であっても 16 文字であるという条件は守られなければいけません。
...Flutter のテスト実行(flutter test
)前後に処理を挟むことができる仕組みが提供されています。
test/
ディレクトリの下に flutter_test_config.dart
を作りましょう。コードの中身は次の通りにします。
以下のコードは SubClass の method の部分でエラーになります。エラーメッセージは以下です。
'SubClass.method' ('void Function(ArgumentOfSubClass)') isn't a valid override of 'SuperClass.method' ('void Function(ArgumentOfSuperClass)').
class SuperClassArg {}
class SubClassArg extends SuperClassArg {}
class SuperClass {
void method(SuperClassArg arg) {
// Do something...
}
}
class SubClass extends SuperClass {
@override
void method(SubClassArg arg) {
// Do something...
}
}
以下のようにすることでエラーは解消します。
...公式ドキュメントの案内によると Dart のコンストラクタは6種類あるようです。
Dart implements many types of constructors. Except for default constructors, these functions use the same name as their class.
- Generative constructors: Creates new instances and initializes instance variables.
- Default constructors: Used to create a new instance when a constructor hasn’t been specified. It doesn’t take arguments and isn’t named.
- Named constructors: Clarifies the purpose of a constructor or allows the creation of multiple constructors for the same class.
- Constant constructors: Creates instances as compile-type constants.
- Factory constructors: Either creates a new instance of a subtype or returns an existing instance from cache.
- Redirecting constructor: Forwards calls to another constructor in the same class.
またコンストラクタと同等の動きをするものとして、インスタンスを返す static メソッドを実装することもできます。これらのコンストラクタを見ていきましょう。
...Flutter の BoxConstraints について。
次のコードでは、画面に何が表示されるでしょうか?
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(
home: SizedBox(
width: 100,
height: 100,
child: ColoredBox(
color: Colors.orange,
),
),
));
}
「一辺 100px」のオレンジ色の四角形」が表示されると思いきや、実際には「画面いっぱいに(つまり画面分の高さと幅の)」オレンジ色が表示されます。
...import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: MyParentWidget(),
));
}
class MyParentWidget extends StatelessWidget {
MyParentWidget({super.key});
final myChildWidgetKey = GlobalKey<MyChildWidgetState>();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Text('${myChildWidgetKey.currentState?.count ?? 0}'),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
myChildWidgetKey.currentState?.increment();
// myChildWidgetKey.currentState?.increment(); を呼び出しても、MyParentWidget 自身は再ビルドされず、
// myChildWidgetKey.currentState?.count の更新が画面に反映されない
// MyParentWidget 自身を再ビルドするために markNeedsBuild(); を呼び出す
(context as Element).markNeedsBuild();
},
),
MyChildWidget(
key: myChildWidgetKey,
),
],
),
);
}
}
class MyChildWidget extends StatefulWidget {
const MyChildWidget({super.key});
@override
State<MyChildWidget> createState() => MyChildWidgetState();
}
class MyChildWidgetState extends State<MyChildWidget> {
int count = 0;
void increment() {
setState(() {
count++;
});
}
@override
Widget build(BuildContext context) {
return const SizedBox();
}
}
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(
home: MyParentWidget(),
));
}
class MyParentWidget extends StatefulWidget {
const MyParentWidget({super.key});
@override
State<MyParentWidget> createState() => _MyParentWidgetState();
}
class _MyParentWidgetState extends State<MyParentWidget> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return MyChildWidget(
count: _count,
increment: _increment,
);
}
}
class MyChildWidget extends StatelessWidget {
const MyChildWidget({
super.key,
required this.count,
required this.increment,
});
final int count;
final VoidCallback increment;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Text('$count'),
IconButton(
icon: const Icon(Icons.add),
onPressed: increment,
),
],
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(
home: MyParentWidget(),
));
}
class MyInheritedWidget extends InheritedWidget {
const MyInheritedWidget({
super.key,
required this.count,
required this.increment,
required super.child,
});
final int count;
final VoidCallback increment;
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) {
return true;
}
static MyInheritedWidget of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>()!;
}
}
class MyParentWidget extends StatefulWidget {
const MyParentWidget({super.key});
@override
State<MyParentWidget> createState() => _MyParentWidgetState();
}
class _MyParentWidgetState extends State<MyParentWidget> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return MyInheritedWidget(
count: _count,
increment: _increment,
child: const MyChildWidget(),
);
}
}
class MyChildWidget extends StatelessWidget {
const MyChildWidget({super.key});
@override
Widget build(BuildContext context) {
final counter = MyInheritedWidget.of(context);
return Scaffold(
body: Column(
children: [
Text('${counter.count}'),
IconButton(
icon: const Icon(Icons.add),
onPressed: counter.increment,
),
],
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(
home: MyParentWidget(),
));
}
class MyParentWidget extends StatefulWidget {
const MyParentWidget({super.key});
@override
State<MyParentWidget> createState() => MyParentWidgetState();
}
class MyParentWidgetState extends State<MyParentWidget> {
int count = 0;
void increment() {
setState(() {
count++;
});
}
@override
Widget build(BuildContext context) {
// const をつけると count が変わっても再ビルドされないため const を外す
// ignore: prefer_const_constructors
return MyChildWidget();
}
}
class MyChildWidget extends StatelessWidget {
const MyChildWidget({super.key});
@override
Widget build(BuildContext context) {
final state = context.findAncestorStateOfType<MyParentWidgetState>()!;
return Scaffold(
body: Column(
children: [
Text('${state.count}'),
IconButton(
icon: const Icon(Icons.add),
onPressed: state.increment,
),
],
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: MyParentWidget(),
));
}
final myParentWidgetKey = GlobalKey<MyParentWidgetState>();
class MyParentWidget extends StatefulWidget {
MyParentWidget() : super(key: myParentWidgetKey);
@override
State<MyParentWidget> createState() => MyParentWidgetState();
}
class MyParentWidgetState extends State<MyParentWidget> {
int count = 0;
void increment() {
setState(() {
count++;
});
}
@override
Widget build(BuildContext context) {
// const をつけると count が変わっても再ビルドされないため const を外す
// ignore: prefer_const_constructors
return MyChildWidget();
}
}
class MyChildWidget extends StatelessWidget {
const MyChildWidget({super.key});
@override
Widget build(BuildContext context) {
final state = myParentWidgetKey.currentState!;
return Scaffold(
body: Column(
children: [
Text('${state.count}'),
IconButton(
icon: const Icon(Icons.add),
onPressed: state.increment,
),
],
),
);
}
}
Flutter の状態管理ライブラリの3トップ(個人的見解)である「Bloc/Cubit」「Provider」「Riverpod」について、カウンターアプリでのコード例を見ていきましょう。
...Dart 言語には assert という機能があります。assert の第一引数が条件式で、こちらが true
であれば何も起きず、false
であれば例外が throw されます。assert は Flutter のデバッグモードでのみ有効になります。リリースビルドを行った際は assert のコードをツリーシェイキングにより取り除かれ、ビルド後のコードからは除外されます。
参考
DevTools でのアプリケーションのサイズ確認について
ツリーシェイキングについて
Flutter のプロジェクトを作成し、以下コードのビルド後のサイズを確認するとします。
...Flutter において Dart の Stream はなくてはならない存在。たとえ開発者が意図的に使っていなかったとしても、ステート管理ライブラリの屋台骨は Stream が支えています。
StreamController()
#
StreamController()
でコントローラを生成し、それに sink.add()
することでストリームにイベントを流すことができます。コントローラを stream.listen()
して、コールバック関数を定義することで、流れてきたイベントを捕捉することができます。stream.listen()
に onError()
、onDone()
を定義することで、それぞれエラー時のイベント、ストリームが閉じられる時のイベントを捕捉することができます。
ロンドン大学で MSc Computer Science: Project モジュールを履修中。
講義内容に関して記録した個人的なスタディノートです。
全 24 週のうち 6〜12 週目の内容を記録します。(6 週目開始:2024 年 5 月 13 日 / 12 週目終了:2024 年 7 月 1 日)
...ロンドン大学で MSc Computer Science: Software design and programming モジュールを履修中。
講義内容に関して記録した個人的なスタディノートです。
全 12 週のうち 6〜12 週目の内容を記録します。(6 週目開始:2024 年 5 月 13 日 / 12 週目終了:2024 年 7 月 1 日)
...既存のクラスのコードそのものには手を入れる事なくメソッドを追加する方法が提供されています。
calculator.dart
class Calculator {
int value = 0;
void add(int a, int b) {
value = a + b;
}
}
calculator_extension.dart
素敵なブログ記事を見かけたので記録。ウィジェットテスト中に、そのときのウィジェットのスクリーンショットを画像で取得することができます。
以下は flutter create
直後のウィジェットテストのコードに対して、スクリーンショットを取得するコードを追加した例です。flutter test
を実行すると screenshots
ディレクトリが作成され、その中にスクリーンショットが格納されます。
ほんとにただの1行メモで恐縮ですが、Flutter の DevTools の説明は以下の動画がヒジョーに分かりやすい!
動画では DevTools でできる以下のことが一通り説明されています。
...StatefulWidget(の State)は debugFillProperties()
メソッドを持っています。これをオーバーライドするとデバッグが少しだけ楽になります。
これを使うことで何がどう変わるのかは以下の Stack Overflow が分かりやすいです。
...現在 Dart には Self 型を表すようなキーワードはありません。キーワードはないものの、Self 型を実現することはでき、具体的には次のように書けば実現可能です。若干奇妙な書き方です。
...flutter create
でアプリを作成すると次のテストファイルが作られています。
myapp/test/widget_test.dart
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
テストを実施するには以下コマンドを実行すれば良いです。
...次のメソッドの実行順番を確認するためにログを出力させてみた。
createState()
build()
setState()
initState()
didChangeDependencies()
didUpdateWidget()
deactivate()
dispose()
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({
super.key,
});
@override
State<MyApp> createState() {
print("MyApp.createState()");
return _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
print("MyApp.setState()");
}
@override
void initState() {
print("_MyAppState.initState()");
super.initState();
}
@override
void didChangeDependencies() {
print("_MyAppState.didChangeDependencies()");
super.didChangeDependencies();
}
@override
void didUpdateWidget(covariant MyApp oldWidget) {
print("_MyAppState.didUpdateWidget()");
super.didUpdateWidget(oldWidget);
}
@override
void deactivate() {
print("_MyAppState.deactivate()");
super.deactivate();
}
@override
void dispose() {
print("_MyAppState.dispose()");
super.dispose();
}
@override
Widget build(BuildContext context) {
print("_MyAppState.build()");
return MaterialApp(
home: Scaffold(
body: Column(
children: [
if (_count < 3) ...[
MyCountWidget(count: _count),
MyButtonWidget(onPress: _increment),
] else
const MyDoneWidget(),
],
),
),
);
}
}
class MyCountWidget extends StatefulWidget {
const MyCountWidget({
super.key,
required this.count,
});
final int count;
@override
State<MyCountWidget> createState() {
print("MyCountWidget.createState()");
return _MyCountWidgetState();
}
}
class _MyCountWidgetState extends State<MyCountWidget> {
@override
void initState() {
print("_MyCountWidgetState.initState()");
super.initState();
}
@override
void didChangeDependencies() {
print("_MyCountWidgetState.didChangeDependencies()");
super.didChangeDependencies();
}
@override
void didUpdateWidget(covariant MyCountWidget oldWidget) {
print("_MyCountWidgetState.didUpdateWidget()");
super.didUpdateWidget(oldWidget);
}
@override
void deactivate() {
print("_MyCountWidgetState.deactivate()");
super.deactivate();
}
@override
void dispose() {
print("_MyCountWidgetState.dispose()");
super.dispose();
}
@override
Widget build(BuildContext context) {
print("_MyCountWidgetState.build()");
return Text("count: ${widget.count}");
}
}
class MyButtonWidget extends StatefulWidget {
const MyButtonWidget({
super.key,
required this.onPress,
});
final VoidCallback onPress;
@override
State<MyButtonWidget> createState() {
print("MyButtonWidget.createState()");
return _MyButtonWidgetState();
}
}
class _MyButtonWidgetState extends State<MyButtonWidget> {
@override
void initState() {
print("_MyButtonWidgetState.initState()");
super.initState();
}
@override
void didChangeDependencies() {
print("_MyButtonWidgetState.didChangeDependencies()");
super.didChangeDependencies();
}
@override
void didUpdateWidget(covariant MyButtonWidget oldWidget) {
print("_MyButtonWidgetState.didUpdateWidget()");
super.didUpdateWidget(oldWidget);
}
@override
void deactivate() {
print("_MyButtonWidgetState.deactivate()");
super.deactivate();
}
@override
void dispose() {
print("_MyButtonWidgetState.dispose()");
super.dispose();
}
@override
Widget build(BuildContext context) {
print("_MyButtonWidgetState.build()");
return IconButton(
icon: const Icon(Icons.add),
onPressed: widget.onPress,
);
}
}
class MyDoneWidget extends StatefulWidget {
const MyDoneWidget({
super.key,
});
@override
State<MyDoneWidget> createState() {
print("MyDoneWidget.createState()");
return _MyDoneWidgetState();
}
}
class _MyDoneWidgetState extends State<MyDoneWidget> {
@override
void initState() {
print("_MyDoneWidgetState.initState()");
super.initState();
}
@override
void didChangeDependencies() {
print("_MyDoneWidgetState.didChangeDependencies()");
super.didChangeDependencies();
}
@override
void didUpdateWidget(covariant MyDoneWidget oldWidget) {
print("_MyDoneWidgetState.didUpdateWidget()");
super.didUpdateWidget(oldWidget);
}
@override
void deactivate() {
print("_MyDoneWidgetState.deactivate()");
super.deactivate();
}
@override
void dispose() {
print("_MyDoneWidgetState.dispose()");
super.dispose();
}
@override
Widget build(BuildContext context) {
print("_MyDoneWidgetState.build()");
return const Text("Done");
}
}
ログ
...Flutter でアニメーションをするにはいろいろな方法がある。こちらの記事がわかりやすい。
上記記事中でも紹介されているが以下の動画が良き。
...なんとなく思いついたのでコードをメモ。
一度作成したウィジェットをキャッシュに保存しておいて、必要になったときだけウィジェットを再構築する。
次の例で行くと、3の倍数かそうでないかで表示を切り替えている。何も考えないと数字がカウントアップされるごとに SanNoBaisuWidget
ウィジェットが build()
メソッドで読んでいる中身のウィジェットを構築されることになるが、本当にウィジェットを再構築する必要があるのは「奇数から偶数」「偶数から奇数」に切り替わるタイミングのみである。
パフォーマンス改善のために、ウィジェットをキャッシュしておいて必要なときだけリビルドしたい、そんなときはこんなユーティリティウィジェットが便利です。
...こんな感じです。
import 'package:flutter/material.dart';
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({
super.key,
});
final Counter _counter = Counter();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyWidget(
counter: _counter,
),
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget({
super.key,
required this.counter,
});
final Counter counter;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ListenableBuilder(
listenable: counter,
builder: (context, child) {
return Text('${counter.count}');
},
),
IconButton(
onPressed: counter.increment,
icon: const Icon(Icons.add),
)
],
),
),
);
}
}
import 'package:flutter/material.dart';
class Counter {
final ValueNotifier<int> count = ValueNotifier<int>(0);
void increment() {
count.value++;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({
super.key,
});
final Counter _counter = Counter();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyWidget(
counter: _counter,
),
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget({
super.key,
required this.counter,
});
final Counter counter;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ValueListenableBuilder(
valueListenable: counter.count,
builder: (context, value, child) {
return Text('${counter.count.value}');
},
),
IconButton(
onPressed: counter.increment,
icon: const Icon(Icons.add),
)
],
),
),
);
}
}
ちなみに ValueNotifier は単一の値のみを保持する場合における ChangeNotifier をよりシンプルに使いやすくしたものです。そのため、機能的には ChangeNotifier で実装した場合とほとんど違いありません。
...Flutter で複数の InheritedWidget の値を参照しているときに、参照先ウィジェットがどのタイミングで再ビルドされるのかを確認したかった。その記録。
結果:build()
は1回だけ実行される。
自己理解ために InheritedWidget
でのデータ保持&アクセスをステップバイステップで実装していきます。
InheritedWidget
で保持するデータに子ウィジェットからアクセスする
#
count
のデータを保持する InheritedWidget
を定義しておき、ルートのウィジェット付近に InheritedWidget
を配置します。こうすることで、InheritedWidget
の配下に位置するウィジェットは .of(context)
で count
を取得できるようになります。
現在このブログの記事は GitLab で管理しており、ブログも Gitlab Pages でホスティングされています。バックアップも兼ねて、ブログのデータを GitHub にミラーリングしておこうと思います。
...ロンドン大学で MSc Computer Science: Project モジュールを履修中。
講義内容に関して記録した個人的なスタディノートです。
全 24 週のうち 1〜5 週目の内容を記録します。(1 週目開始:2024 年 4 月 8 日 / 5 週目終了:2024 年 5 月 12 日)
...