Open12

Flutter を理解する

nukopynukopy

Flutter プロジェクト初期化

まずは flutter create を実行した後のプロジェクトの初期構造を見ていく。

プラットフォーム対応

このプロジェクト構造は、Flutter のクロスプラットフォーム性を反映している。初期状態で以下のプラットフォームに対応している:

  • Android
  • iOS
  • Web
  • Linux
  • macOS
  • Windows

各プラットフォームディレクトリには、そのプラットフォーム固有の設定やネイティブコードが含まれている。

主要なディレクトリとファイル

flutter create コマンドで生成された初期プロジェクト構造は以下のようになっている:

  1. lib/ - Dart コードの中心となるディレクトリ
    • main.dart - アプリケーションのエントリーポイント
  2. android/ - Android プラットフォーム固有のコードと設定
    • Gradle の設定ファイル
    • マニフェストファイル
    • リソースファイル
  3. ios/ - iOS プラットフォーム固有のコードと設定
    • Xcode プロジェクトファイル
    • Info.plist
    • アセット
  4. web/ - Web プラットフォーム用のファイル
    • index.html
    • アイコンとマニフェスト
  5. linux/, macos/, windows/ - デスクトッププラットフォーム用のファイル
  6. test/ - テストコードを配置するディレクトリ
    • widget_test.dart - サンプルのウィジェットテスト
  7. pubspec.yaml - プロジェクトの依存関係と設定を管理するファイル
    • パッケージの依存関係
    • アセットの設定
    • フォントの設定
  8. pubspec.lock - 実際にインストールされた依存関係のバージョンを固定するファイル
  9. analysis_options.yaml - Dart コード解析の設定ファイル
    • リントルールの設定
  10. .metadata - Flutter ツールが使用するプロジェクトのメタデータ
  11. .gitignore - Git バージョン管理から除外するファイルとディレクトリのリスト

隠しディレクトリ

  1. .dart_tool/ - Dart ツールが使用する一時ファイル
  2. .idea/ - IntelliJ IDEA の設定ファイル
  3. build/ - ビルド成果物が格納されるディレクトリ
nukopynukopy

エントリーポイント

  • アプリケーションのエントリーポイントとなるソースコードは lib/main.dart
  • main 関数がエントリーポイントになる。
  • main 関数内の runApp() 関数は Widget 型を受け取ってアプリケーションを実行する。
  • Flutter の UI の構築は Widget 型を中心としてるみたい
  • 全ての UI 要素は Widget 型を継承している
  • Flutter の基本的な書き方は、
    • StatelessWidget / StatefulWidget 型を継承したクラスを定義する(build メソッドを持つ主要な Widget 型の抽象クラスはこの 2 つ。)
    • Widget 型を返す build メソッドを override して、Widget ツリーを書いて Widget を返す
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // TRY THIS: Try running your application with "flutter run". You'll see
        // the application has a purple toolbar. Then, without quitting the app,
        // try changing the seedColor in the colorScheme below to Colors.green
        // and then invoke "hot reload" (save your changes or press the "hot
        // reload" button in a Flutter-supported IDE, or press "r" if you used
        // the command line to start the app).
        //
        // Notice that the counter didn't reset back to zero; the application
        // state is not lost during the reload. To reset the state, use hot
        // restart instead.
        //
        // This works for code too, not just values: Most code changes can be
        // tested with just a hot reload.
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // TRY THIS: Try changing the color here to a specific color (to
        // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
        // change color while the other colors stay the same.
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          //
          // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
          // action in the IDE, or press "p" in the console), to see the
          // wireframe for each widget.
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
nukopynukopy

StatelessWidget と StatefulWidget

  • StatelessWidget
    • 状態を持たないウィジェット
  • StatefulWidget
    • 状態を持つウィジェット

どゆこと?

ソースコードを見てみる

Widget


abstract class Widget extends DiagnosticableTree {
  /// Initializes [key] for subclasses.
  const Widget({this.key});
  final Key? key;

  
  
  Element createElement();

  
  String toStringShort() {
    final String type = objectRuntimeType(this, 'Widget');
    return key == null ? type : '$type-$key';
  }

  
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  
  
  bool operator ==(Object other) => super == other;

  
  
  int get hashCode => super.hashCode;

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key;
  }

  static int _debugConcreteSubtype(Widget widget) {
    return widget is StatefulWidget
        ? 1
        : widget is StatelessWidget
        ? 2
        : 0;
  }
}

抽象クラス StatelessWidget

  • packages/flutter/lib/src/widgets/framework.dart
abstract class StatelessWidget extends Widget {
  const StatelessWidget({super.key});

  
  StatelessElement createElement() => StatelessElement(this);

  
  Widget build(BuildContext context);
}

抽象クラス StatefulWidget

注意が必要なのは、StatefulWidget 自体は build メソッドを持っていないこと。
代わりに、関連する State クラスが build メソッドを実装する。

  • packages/flutter/lib/src/widgets/framework.dart
abstract class StatefulWidget extends Widget {
  const StatefulWidget({super.key});

  
  StatefulElement createElement() => StatefulElement(this);

  
  
  State createState();
}

抽象クラス State

  • packages/flutter/lib/src/widgets/framework.dart

abstract class State<T extends StatefulWidget> with Diagnosticable {
  T get widget => _widget!;
  T? _widget;

  _StateLifecycle _debugLifecycleState = _StateLifecycle.created;

  bool _debugTypesAreRight(Widget widget) => widget is T;

  BuildContext get context {
    assert(() {
      if (_element == null) {
        throw FlutterError(
          'This widget has been unmounted, so the State no longer has a context (and should be considered defunct). \n'
          'Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.',
        );
      }
      return true;
    }());
    return _element!;
  }

  StatefulElement? _element;

  bool get mounted => _element != null;

  
  
  void initState() {
    assert(_debugLifecycleState == _StateLifecycle.created);
    if (kFlutterMemoryAllocationsEnabled) {
      FlutterMemoryAllocations.instance.dispatchObjectCreated(
        library: _flutterWidgetsLibrary,
        className: '$State',
        object: this,
      );
    }
  }

  
  
  void didUpdateWidget(covariant T oldWidget) {}

  
  
  void reassemble() {}

  
  void setState(VoidCallback fn) {
    assert(() {
      if (_debugLifecycleState == _StateLifecycle.defunct) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('setState() called after dispose(): $this'),
          ErrorDescription(
            'This error happens if you call setState() on a State object for a widget that '
            'no longer appears in the widget tree (e.g., whose parent widget no longer '
            'includes the widget in its build). This error can occur when code calls '
            'setState() from a timer or an animation callback.',
          ),
          ErrorHint(
            'The preferred solution is '
            'to cancel the timer or stop listening to the animation in the dispose() '
            'callback. Another solution is to check the "mounted" property of this '
            'object before calling setState() to ensure the object is still in the '
            'tree.',
          ),
          ErrorHint(
            'This error might indicate a memory leak if setState() is being called '
            'because another object is retaining a reference to this State object '
            'after it has been removed from the tree. To avoid memory leaks, '
            'consider breaking the reference to this object during dispose().',
          ),
        ]);
      }
      if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('setState() called in constructor: $this'),
          ErrorHint(
            'This happens when you call setState() on a State object for a widget that '
            "hasn't been inserted into the widget tree yet. It is not necessary to call "
            'setState() in the constructor, since the state is already assumed to be dirty '
            'when it is initially created.',
          ),
        ]);
      }
      return true;
    }());
    final Object? result = fn() as dynamic;
    assert(() {
      if (result is Future) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('setState() callback argument returned a Future.'),
          ErrorDescription(
            'The setState() method on $this was called with a closure or method that '
            'returned a Future. Maybe it is marked as "async".',
          ),
          ErrorHint(
            'Instead of performing asynchronous work inside a call to setState(), first '
            'execute the work (without updating the widget state), and then synchronously '
            'update the state inside a call to setState().',
          ),
        ]);
      }
      // We ignore other types of return values so that you can do things like:
      //   setState(() => x = 3);
      return true;
    }());
    _element!.markNeedsBuild();
  }

  
  
  void deactivate() {}

  
  
  void activate() {}

  
  
  void dispose() {
    assert(_debugLifecycleState == _StateLifecycle.ready);
    assert(() {
      _debugLifecycleState = _StateLifecycle.defunct;
      return true;
    }());
    if (kFlutterMemoryAllocationsEnabled) {
      FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
    }
  }

  // ここだね
  
  Widget build(BuildContext context);

  
  
  void didChangeDependencies() {}

  
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    assert(() {
      properties.add(
        EnumProperty<_StateLifecycle>(
          'lifecycle state',
          _debugLifecycleState,
          defaultValue: _StateLifecycle.ready,
        ),
      );
      return true;
    }());
    properties.add(ObjectFlagProperty<T>('_widget', _widget, ifNull: 'no widget'));
    properties.add(
      ObjectFlagProperty<StatefulElement>('_element', _element, ifNull: 'not mounted'),
    );
  }
}
nukopynukopy

注意が必要なのは、StatefulWidget 自体は build メソッドを持っていないこと。
代わりに、関連する State クラスが build メソッドを実装する。

これに関しては、プロジェクト初期化時の lib/main.dart 実装をみるとよく分かる。

下記コードでは、StatefulWidget を継承した MyHomePage クラスでは build メソッドを実装せず、State を継承した _MyHomePageState クラスで build メソッドをオーバーライドして実装している。

State の継承を見ると、extends State<MyHomePage> のように継承している。State はジェネリックな抽象クラスで、型パラメータとして StatefulWidget を継承したクラスを受け取る必要がある。

main.dart
...
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // TRY THIS: Try changing the color here to a specific color (to
        // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
        // change color while the other colors stay the same.
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          //
          // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
          // action in the IDE, or press "p" in the console), to see the
          // wireframe for each widget.
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
nukopynukopy

センサー系

問題

ensors_plus で加速度、ジャイロ取れるようになったが、200ms とかになってしまう
update interval を設定する API ないっぽい?

他の選択肢

https://zenn.dev/hrimfaxi_tpw/scraps/2706581a6a9974

nukopynukopy

ロギング

以下が参考になった:

https://zenn.dev/honda9135/articles/181ea0d490c073

  • info, warning は Firebase Analytics
  • error, fatal, クラッシュ は Firebase Crashlytics

という棲み分けが良さそう。
Sentry でも良いかもだけど、ネット漁った感じは Firebase Crashlytics が採用されている例の方が多かった感触。

nukopynukopy

WebView

WebView のパッケージの有名どころ 2 選

nukopynukopy

ネイティブ、WebView ブリッジによる認証フロー

既存の Web アプリのクッキーによるセッション管理を活用するパターン

  1. ネイティブアプリ起動
  2. WebView でログイン画面表示
  3. WebView でユーザ名、パスワードでログイン
  4. サーバ側でセッション ID 発行
  5. レスポンスに Cookie(セッション ID)が含まれる
  6. WebView → ネイティブへクッキーの共有
  7. ネイティブの shared preferences を使ってローカルに永続化
  8. 以後、ネイティブから API へアクセスするときに WebView から共有されたクッキーを使用して認証が必要な API へアクセスする
  9. (クッキーの期限(=セッションの期限)が切れて API からエラー返ってきたときは、モバイル側を強制ログアウトしてログイン画面に遷移するフローを忘れずに)