省エネ

Flutter、vue3修行中。

【Flutter】freezedではまった話

先日、freezedでハマって小一時間を無駄に過ごしてしまったのでここに書いて供養したいと思います。

今やってるお仕事ではriverpodを使っていて、screenに対してview_modelを作成し、state管理を行っています。
view_modelの持っているstateをfreezedで作っているのですが、そこで値の変更ができなくてはまった話です。

class HomeScreen extends ConsumerWidget {
    const HomeScreen({Key? key}) : super(key: key);
    
    @override
    Widget build(BuildContext context, WidgetRef ref) {

        final viewModel = ref.watch(homeViewModelProvider);
        return viewModel.when(
            data: (state) => Scaffold(// 省略), 
            error: (_, __) => ErrorView(), 
            loading: () => const Center(child: CircularProgressIndicator()));
    }
}
@riverpod
class HomeViewModel extends _$HomeViewModel {
    @override
     Future<HomeState> build() {
        return HomeState();
     }
}

@freezed
class HomeState with _$HomeState {
    const factory HomeState() = _HomeState;
}

と、こんな感じでfreezedを使っています。
ここで、HomeStateは何か値を持ち、ボタンをタップされたらViewModelで値を変更するという実装があるとします。

@riverpod
class HomeViewModel extends _$HomeViewModel {
    @override
     Future<HomeState> build() {
        return HomeState(hogehoge: 'hogehoge');
     }

    Future<void> onTapButton({required String fugafuga}) async {
       final currentState = await future;
        state = await AsyncValue.guard(() async {
            return currentState.copyWith(hogehoge: fugafuga);
        });
    }
}

@freezed
class HomeState with _$HomeState {
    const factory HomeState({
        required String hogehoge,
    }) = _HomeState;
}

と、こんな感じでhogehogeの値を変更しようとした時にエラーが起きました。
エラーの内容は Null check operator used on a null value

同じような実装は過去にいくつもしているので、なぜここだけこんなエラーが出るのか分からずに半日くらいコードと睨めっこしてしまいました。

結論としては、HomeStateの書き方に問題がありました。
実際に私が書いていたコードはこうでした。

@freezed
class HomeState with _$HomeState {
    const factory HomeState({
        required String hogehoge,
        required}) = _HomeState;
}

お分かりいただけたでしょうか?
hogehogeの次に謎のrequiredがありました。
場所が場所だけにこの謎のrequiredが書かれている事に気づかなかった事。
そして、静的コンパイルでもエラーが出ずにジェネレートもできてしまうんですよね。
さらに、初期値は普通に入れれるので初回の画面表示は問題なくできてしまうんですよね。

これが邪魔して、nullの値があるよっていうエラーがで続けていました。

そんなこんなでfreezedを使う時には余計なコードがないか気をつけようねっていう話でした。

flutter Geminiで簡単にai chatが作れた話

こんにちは。
Geminiを使った簡単なアプリを作ってみたので、ここにコードを残したいと思います。

環境

Flutter: 3.19.1 
Dart: 3.3.0

pub.dev こちらのライブラリを使いました。

Api Kyeはここで作成します。 aistudio.google.com

実装

pubspec.yml

name: gemini_sample
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1

environment:
  sdk: '>=3.3.0 <4.0.0'

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter
  google_generative_ai: ^0.2.1


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.6

dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^3.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

main.dart

import 'package:flutter/material.dart';
import 'package:google_generative_ai/google_generative_ai.dart';

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Gemini Sample App'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  final _controller = TextEditingController();
  final _focus = FocusNode();
  bool isLoading = false;

  final model = GenerativeModel(
      model: 'gemini-pro', apiKey: '作成したApi Key');

  final List<String> answer = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Stack(
        children: [
          ListView.separated(
              itemCount: answer.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(answer[index]),
                );
              },
              separatorBuilder: (context, index) {
                return const Divider();
              }),
          if (isLoading)
            const Center(
              child: CircularProgressIndicator(),
            ),
        ],
      ),
      bottomSheet: Padding(
        padding: const EdgeInsets.all(8),
        child: Row(
          children: [
            Expanded(
                child: TextField(
              focusNode: _focus,
              controller: _controller,
            )),
            IconButton(
                onPressed: () {
                  _focus.unfocus();
                  setState(() {
                    isLoading = true;
                  });
                  final content = [Content.text(_controller.text)];
                  model.generateContent(content).then((value) {
                    setState(() {
                      answer.add(value.text ?? '');
                      _controller.clear();
                      isLoading = false;
                    });
                  });
                },
                icon: const Icon(Icons.send)),
          ],
        ),
      ),
    );
  }
}

ほぼReadme通りです! これで簡単にai chatが作れました。

プロンプトは「今夜の献立を一緒に考えて」と入れてみました。
お天気とか聞いてみたけど、そういうのは答えてくれませんでした。

そんな感じで簡単にchatができました。
何か面白いアプリ作れそうな気がしますね!

【Flutter】【Flame】初めてのゲーム開発

こんにちは。
本日はFlutter Flameで作るゲーム開発について書きたいと思います。

FlameとはFlutterで使えるゲームエンジンです。

pub.dev

これをいつも通りpubspec.ymlに追加するだけでゲームエンジンが使えてしまいます。
簡単ですね。
普段Flutterを使っている方ならすぐに使えるようになると思います。

github.com

コードはこちらに公開しています。
このアプリを作るにあたってCodelabの講座を受けました。

codelabs.developers.google.com

サンプルコードを落としてきて書いてある通りにコピーしていくとゲームができあがります!

この講座を受けてから、DoodleDashのコードを真似してゲームを作成していきました。

なぜこのゲームを作ったかというと、こちらの記事をみて初心者が作るべきゲームの最初の1つに良いかなと思ったからです。

2dgames.jp

作ったものはGitHubPagesで公開しています。

PWAにも対応しているのでスマホで開けばアプリとしてインストールする事もできます。

qkuronekop.github.io

ぜひぜひ遊んでみてくださいね。

次回作として、クリックゲームを作成中です。

初心者が作成すべきゲーム20選、飽きるまで順番に作っていこうと思います。

【Flutterやろうよ!】初級編 その3〜スクロール〜

こんにちは。
前回、前々回と縦並び、横並びについて書いてきました。

qkuronekop.hatenablog.jp

Column()について。

qkuronekop.hatenablog.jp

Row()について。

本日はスクロールについて書いていきたいと思います。

はみだし

上の画像のように横に沢山要素を並べていくといずれはみ出します。

はみ出さずに沢山並べるにはどうしたらいいでしょうか?
そうです!
スクロールさせます。

Before

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('基本の縦並び'),
      ),
      body: Container(
        width: double.infinity,
        color: Colors.yellow,
        child: const Row(
          children: [
            ColoredBox(
              color: Colors.redAccent,
              child: Text('1行目'),
            ),
            ColoredBox(
              color: Colors.orange,
              child: Text('2行目'),
            ),
            ColoredBox(
              color: Colors.blue,
              child: Text('3行目'),
            ),
            ColoredBox(
              color: Colors.redAccent,
              child: Text('4行目'),
            ),
            ColoredBox(
              color: Colors.orange,
              child: Text('5行目'),
            ),
            ColoredBox(
              color: Colors.blue,
              child: Text('6行目'),
            ),
            ColoredBox(
              color: Colors.redAccent,
              child: Text('7行目'),
            ),
            ColoredBox(
              color: Colors.orange,
              child: Text('8行目'),
            ),
            ColoredBox(
              color: Colors.blue,
              child: Text('9行目'),
            ),
            ColoredBox(
              color: Colors.redAccent,
              child: Text('10行目'),
            ),
            ColoredBox(
              color: Colors.orange,
              child: Text('11行目'),
            ),
            ColoredBox(
              color: Colors.blue,
              child: Text('12行目'),
            )
          ],
        ),
      ),
    );
  }
}

After

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('基本の縦並び'),
      ),
      body: Container(
        width: double.infinity,
        color: Colors.yellow,
        child: const SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Row(
            children: [
              ColoredBox(
                color: Colors.redAccent,
                child: Text('1行目'),
              ),
              ColoredBox(
                color: Colors.orange,
                child: Text('2行目'),
              ),
              ColoredBox(
                color: Colors.blue,
                child: Text('3行目'),
              ),
              ColoredBox(
                color: Colors.redAccent,
                child: Text('4行目'),
              ),
              ColoredBox(
                color: Colors.orange,
                child: Text('5行目'),
              ),
              ColoredBox(
                color: Colors.blue,
                child: Text('6行目'),
              ),
              ColoredBox(
                color: Colors.redAccent,
                child: Text('7行目'),
              ),
              ColoredBox(
                color: Colors.orange,
                child: Text('8行目'),
              ),
              ColoredBox(
                color: Colors.blue,
                child: Text('9行目'),
              ),
              ColoredBox(
                color: Colors.redAccent,
                child: Text('10行目'),
              ),
              ColoredBox(
                color: Colors.orange,
                child: Text('11行目'),
              ),
              ColoredBox(
                color: Colors.blue,
                child: Text('12行目'),
              )
            ],
          ),
        ),
      ),
    );
  }
}

スクロール
SingleChildScrollView() を入れた事によってはみ出す事がなくなりましたね。
scrollDirection: Axis.horizontal,とする事で横スクロールになります。
デフォルトは縦なので、縦方向にスクロールさせたい場合にはこの設定を消すか、scrollDirection: Axis.vertical,にしてください!

こんな時にもスクロール!

入力画面
こんな画面があったとします。
ログイン画面とかプロフィール入力画面とか、こんなのありそうですよね。

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('基本の縦並び'),
      ),
      body: Column(
        children: [
          const SizedBox(
            height: 24,
          ),
          const Text('メール'),
          const TextField(
            decoration: InputDecoration(hintText: 'メールアドレス'),
          ),
          const SizedBox(
            height: 16,
          ),
          const Text('パスワード'),
          const TextField(
            decoration: InputDecoration(hintText: 'パスワード'),
          ),
          const SizedBox(
            height: 16,
          ),
          const Text('名前'),
          const TextField(
            decoration: InputDecoration(hintText: '名前'),
          ),
          const SizedBox(
            height: 16,
          ),
          const Text('ニックネーム'),
          const TextField(
            decoration: InputDecoration(hintText: 'ニックネーム'),
          ),
          const SizedBox(
            height: 16,
          ),
          const Text('お住まい'),
          const TextField(
            decoration: InputDecoration(hintText: 'お住まい'),
          ),
          const SizedBox(
            height: 32,
          ),
          ElevatedButton(onPressed: () {}, child: Text('アカウント登録!')),
        ],
      ),
    );
  }
}

一見画面内に収まっているように見えますが、

はみだしっ

入力の為にキーボードが上がってくるとはみ出してしまいます。
なので、こうです。

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('基本の縦並び'),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            const SizedBox(
              height: 24,
            ),
            const Text('メール'),
            const TextField(
              decoration: InputDecoration(hintText: 'メールアドレス'),
            ),
            const SizedBox(
              height: 16,
            ),
            const Text('パスワード'),
            const TextField(
              decoration: InputDecoration(hintText: 'パスワード'),
            ),
            const SizedBox(
              height: 16,
            ),
            const Text('名前'),
            const TextField(
              decoration: InputDecoration(hintText: '名前'),
            ),
            const SizedBox(
              height: 16,
            ),
            const Text('ニックネーム'),
            const TextField(
              decoration: InputDecoration(hintText: 'ニックネーム'),
            ),
            const SizedBox(
              height: 16,
            ),
            const Text('お住まい'),
            const TextField(
              decoration: InputDecoration(hintText: 'お住まい'),
            ),
            const SizedBox(
              height: 32,
            ),
            ElevatedButton(onPressed: () {}, child: Text('アカウント登録!')),
          ],
        ),
      ),
    );
  }
}

スクロールして解決だ!
これで、キーボードが出ても大丈夫。

画面サイズとか色々あると思うので、小さい端末でもはみださないように入力のある画面にはスクロールを入れておくと安心です。

まとめ

スクロールの使い所、かがだったでしょうか?
これではみ出さないUIが作れる様になりましたね。
次回はスクロールつながりでListViewについて書いていきたいと思います。

【Flutterやろうよ!】初級編 その2〜横並び〜

こんにちは。
Flutter普及記事続きを書いていきたいと思います。
前回はこちら

qkuronekop.hatenablog.jp

本日は要素を横に並べていく方法について書きたいと思います。 基本的にはColumn()と一緒です。 横に並べるにはRow()を使います。

Row(
          children: [
            ColoredBox(
              color: Colors.redAccent,
              child: Text('1行目'),
            ),
            ColoredBox(
              color: Colors.orange,
              child: Text('2行目'),
            ),
            ColoredBox(
              color: Colors.blue,
              child: Text('3行目'),
            ),
          ],
        )

Row()

Column()同様に、MainAxisAlignment.centerで中央にMainAxisAlignment.endで右よせできます。
MainAxisAlignment.spaceBetweenMainAxisAlignment.spaceEvenlyMainAxisAlignment.spaceAroundも同様の効果があります。

spaceBetween
これはspaceBetween。
spaceEvenly
これはspaceEvenly。
spaceAround
これはspaceAround。

この辺をうまく使っていい感じのUIが作れるといいですね。

今、色の付いている部分がRow()の子要素なのですが、子要素の大きさはテキストの文字長と同じ大きさになっていますね。
これを親の要素の大きさに合わせて広げてみます。

Row(
          children: [
            Expanded(
                child: ColoredBox(
              color: Colors.redAccent,
              child: Text('1行目'),
            )),
            Expanded(
                child: ColoredBox(
              color: Colors.orange,
              child: Text('2行目'),
            )),
            Expanded(
                child: ColoredBox(
              color: Colors.blue,
              child: Text('3行目'),
            )),
          ],
        )

Exoanded
黄色の部分(親要素)が見えなくなり、赤、オレンジ、青の要素が画面いっぱいに広がりましたね。
Expanded()を使うと親ウィジェット内で利用可能なすべての空き領域を埋めてくれます。
3つある子要素全てをExpanded()にしたので3つ全てが等間隔に広がりましたが、1つだけをExpanded()にするとこんな感じでExpanded()した要素だけが空き空間を埋めます。

Row(
          children: [
            Expanded(child: ColoredBox(
              color: Colors.redAccent,
              child: Text('1行目'),
            )),
            ColoredBox(
              color: Colors.orange,
              child: Text('2行目'),
            ),
            ColoredBox(
              color: Colors.blue,
              child: Text('3行目'),
            ),
          ],
        )

Expandedの効果

まとめ

本日は、Row()Expanded()```について書きました。 ```Column()```の子要素でもExpanded()```は同じ効果を縦方向に対して発揮します。
この辺をうまく使っていい感じのUIを作ってくださいね!
それでは、次回はスクロールについて書いていきたいと思います。

最後に、スクショのタイトルが「縦並び」のままになってしまっていますが、撮り直すのが面倒なのでこのままにします。
「基本の横並び」に読み替えてください。

【Flutterやろうよ!】初級編 その1〜縦に並べる〜

こんにちは。
最近、Flutter流行ってきてるなぁと思う今日この頃です。
私自身はそろそろFlutterのお仕事を始めて3年くらいになります。

最近はブログなんかのドキュメントも充実し始めているかと思いますが、日本語のFlutter記事の普及に貢献したいと思いますので、私もFlutterに関する事をぼちぼち書いていこうと思います。

初級編と題しまして、簡単なUIから始めていこうかと思います。
これを機にFlutter使ってみたいと思う人が増えてくれると嬉しいです。

まずは初級編その1の前にFlutterで文字を表示するWidgetText()というものがあります。

Text()は第一引数に表示したい文字(String)を渡します。

Text('aaaaa')

こんな感じです。

これを縦に並べるにはColumn()というWidgetを使います。

Column(
        children: [
          Text('1行目'),
          Text('2行目'),
          Text('3行目'),
        ],
      )

childrenにはList<Widgte>を渡してあげます。
ちなみに、他の言語にもあるかと思いますが、引数を名前付きで渡せるこの機能を「名前付き引数」と言います。
そのままですね。
これを使うと順番を必ずしも呼び出し元と同じ順番にしなくても大丈夫なんですよ。
便利ですね。

そんなこんなでこの様に縦並びのUIができました。

Column()を使った画面

この画面全体のコードはこうなります。

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('基本の縦並び'),),
      body: const Column(
        children: [
          Text('1行目'),
          Text('2行目'),
          Text('3行目'),
        ],
      ),
    );
  }
}

Scaffold()は画面の一番上のWidgetです。
Scaffold()にはAppBar()(一番上のヘッダーの部分)やFloatingActionButton()(flutterのサンプルコードにあるような右下のボタン)、BottomSheet()(画面下に固定されるUI)などの便利な機能がありますので、画面の一番上に置いておく事をおすすめします。
Scaffold()のbodyはAppBarから下の部分です。

ここに画面の中身を書いていきます。

Column()にはこの縦並びのリストをどう表示させるかという命令を与える事ができます。

MainAxisAlignment

Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('1行目'),
          Text('2行目'),
          Text('3行目'),
        ],
      )

センター
こんな風にmainAxisAlignmentに引数を渡すと縦方向にどこに表示させるか指定できます。
DefaultはMainAxisAlignment.startです。

Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          Text('1行目'),
          Text('2行目'),
          Text('3行目'),
        ],
      )

エンド

Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text('1行目'),
            Text('2行目'),
            Text('3行目'),
          ],
        )

between
要素の間に等間隔のスペースが開きます。

Column(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            Text('1行目'),
            Text('2行目'),
            Text('3行目'),
          ],
        )

around
要素の上下に等間隔のスペースが開きます。

Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Text('1行目'),
            Text('2行目'),
            Text('3行目'),
          ],
        )

evenly
全てのスペースが等間隔に開きます。

この辺をうまく使いこなせる様になるといい感じのUIができるようになると思います!

MainAxisSize

Column(
        mainAxisAlignment: MainAxisAlignment.end,
        mainAxisSize: MainAxisSize.min,
        children: [
          Text('1行目'),
          Text('2行目'),
          Text('3行目'),
        ],
      )

mainAxisSizeはColumn()の縦方向の大きさを設定できます。
DefaultはMainAxisSize.maxです。
MainAxisSize.maxは画面の空いているサイズいっぱいになっているのに対してminにすると、要素分のサイズになります。

min
Column()のサイズが中身の要素分しかなくなってしまったので、MainAxisAlignment.end,にしても画面の下部に表示されず、上部に表示されていますね。

わかりやすく色をつけてみました。
赤色の部分がColumn()の範囲です。

色付

CrossAxisAlignment

CrossAxisAlignmentは横方向のどこへ表示するかという設定です。
Column()の横方向は要素の最大サイズになっています。
これを画面の端まで広げるには親の要素で画面いっぱいという設定をしてあげるというのが一つの手法です。

Container(
        width: double.infinity,
        color: Colors.yellow,
        child: const Column(
          mainAxisAlignment: MainAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            ColoredBox(
              color: Colors.redAccent,
              child: Text('1行目'),
            ),
            ColoredBox(
              color: Colors.orange,
              child: Text('2行目'),
            ),
            ColoredBox(
              color: Colors.blue,
              child: Text('3行目'),
            ),
          ],
        ),
      )

この様に親の要素で画面いっぱいに広げてあげる事ができます。
黄色の部分がColum()の大きさです。

Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            ColoredBox(
              color: Colors.redAccent,
              child: Text('1行目'),
            ),
            ColoredBox(
              color: Colors.orange,
              child: Text('2行目'),
            ),
            ColoredBox(
              color: Colors.blue,
              child: Text('3行目'),
            ),
          ],
        )

CrossAxisAlignment.start,を設定する事で要素を左よせできます。
同様にCrossAxisAlignment.centerで中央にCrossAxisAlignment.endで右寄せにできます。
DefaultはCrossAxisAlignment.centerです。

まとめ

Column()の使い方、なんとなくお分かりいただけたでしょうか?
次回は横方向に要素を並べる方法について書いて行こうと思います。

supabase+Flutterでアプリを作りました

こんにちは。
久々の更新です。
更新がなさすぎて広告が出ちゃったので最近作ったアプリについて書きたいと思います。

成果物

play.google.com

植物の観察日記をつけるアプリ「PlantPal」。
まだ全然DLされていないのでよかったら使ってください。

Supabaseとは

supabase.com

supabaseとはBaaSです。
認証、RDB、strage、functionなんかが使えます。
Firebaseに比べると機能は少ないのですが、firebaseとの大きな違いはRDBが使える事でしょうか。
このデータベース、リアルタイムDBとしても使えるのでなかなか便利です。
PlantPalでは認証、RDB、strageの機能を無料枠で使っています。

Flutterへの導入

pub.dev

ちゃんとflutter用のプラグインが用意されているので導入は楽ちんです。
Readmeを読みながら進めれば得にハマる事なく導入できるかと思います。

ちなみに導入時点での環境は↓です。

Flutter version: 3.10.3
supabase_flutter: 1.8.1

認証

結論から言うと、認証は結構簡単にできました。
PlantPalではメール+パスワードでの認証機能を導入しています。

認証の種類も豊富です。
ただ、日本においてはLineやYahoo認証が人気らしいのでこの辺ができないのはちょっと残念。
Firebaseみたいにカスタム認証とかはないらしいです。(2023/6/13現在)
それと、匿名ログインみたいな機能もないようでそこはちょっと使いにくいところですね。
一部の画面はログインなしで見れるけど、投稿とかバックアップはログインが必要ですみたいなアプリだと使えないですね。

SupabaseClient get supabase => Supabase.instance.client;
await supabase.auth
        .signUp(email: request.email, password: request.password);

メール+パスワード認証に関してはこのくらいのコードで完結するのでアプリ側の実装は簡単です。

Database

SQLが書ける人はデータベースは使うのはとても簡単だと思います。
例えばこれはPlantPalで日記を挿入するためのコードです。

SupabaseClient get supabase => Supabase.instance.client;
final uid = supabase.auth.currentUser!.id;
await supabase.from('diary').insert({
  'date': diary.date.toIso8601String(),
  'image': image,
  'title': diary.title,
  'memo': diary.memo,
  'user_id': uid
});

更に、supabase側の管理画面でpolicyを設定しなければなりません。
このDBにinsert,select,delete,updateできるのは認証済のユーザーなのか誰でもアクセスしていいのかを記述した設定を保存しなければなりません。

ちなみにテーブルは管理画面上で作成しました。
Flutter側から作成することもできそうです。

strage

PlantPalは植物の写真を保存できる機能を有しています。
写真はsupabaseのstrageに保存しています。

SupabaseClient get supabase => Supabase.instance.client;
final image = await supabase.storage.from('diary_photo').upload(
    'image.png', file);

strageもDBと同じようなコードで保存できます。
File形式で保存する必要があるので、カメラやImagePickerから取得できるデータXFileの場合にはFile形式にする必要がありそうです。

strageもDB同様にpolicyの設定が必要です。

結論

導入が楽なのですごくよかったです。 Firebaseと比べられる事が多いsupabaseですが、やっぱりRDBが使えるのが大きな特徴ではないでしょうか。
正直な感想を言うと、ちょっと機能が物足りないなーという感じがありますね。
あと、policyの設定方法がよくわからない。
ドキュメントもちょっとわかりにくいんですよね。
もうちょっと機能やドキュメントが充実してきたらお金払って使いたいなーという感じです。