🤐

Flutterを学習するならどうすれば良いのか?

2024/01/16に公開
3

実は世の中に答えはある2個覚えるだけ

初心者やFlutterを始めたエンジニアってどうやってFlutterを勉強してるのでしょうか???
多分公式のサイトを見たり、ZennやQiitaの記事に、Udemyでしょうね。

これで勉強すればなんとかアプリは作れるようになる😅
https://flutter.dev/

https://zenn.dev/topics/flutter

でも仕事だと作れても悩みが出てくる

Flutterでお仕事をしてもう長いってほどではないですが、Flutter + Firebaseでも一応仕事はできる。後は、APIと繋ぐとか???
他に必要な技術といえば、riverpod + Freezed + go_router他には、auto_routeかな...

フォルダ構成だけど、modelFreezedを配置して、providerriverpodのコードをおけば良い。pagesとかviewsの中に、画面のファイルとwidgetとかcomponentの中に、コンポーネントを作れば、まあいいかな〜と思ってる人が多い気がする。

でも実はこれではダメなパターンだったりする。

何をすれば良いのか?

まず作って完成させれば良いのだけど、設計のお話が出てくることがある。技術者がいる会社は設計のお話が出てくる。コードの再利用性とか、疎結合で書いてる。シングルトンを使う。APIの仕様書を書くとか?
他にはテストですかね。

設計は難しいのであまり触れないでおこう💦

Flutterを勉強して開発ができるようになるには、まずやることですけど、2個ぐらいかな〜と思ってます。

それは?

  1. Object思考を理解する.
  2. ライフサイクルを理解する.

これだけです。意外でしょ〜。私もできてるか怪しいが...

どんな情報に触れれば良い?

ソフトウェアエンジニアの人に聞いた方がいいかもですね。私が聞いた話題だと、SwiftやKotlinだと内部のコードを見るとか、ライフサイクルを理解する。内部のコードを見るのは変態のすることなので一旦おいておいて、先ほど紹介した、1と2を学習する手順を学びましょう。

🟦Object思考を理解する.

なんかよくJavaの本を見てくださいと聞きます。C#の本でも良いと思う。知り合いの人は、昔それでオブジェクト思考を覚えていた。Webの人なら、同じAlt.jsのTypeScrptが良さそう。データ型があって、抽象クラスがあれば良いと思う。

やることは同じですから。class, Enum, if, switch, for, while, 変数とfunction他にも色々あるがやることは同じ。

まずは覚えて欲しいのは、変数で値を保持するとか、処理は関数にまとめる、クラスを使って、変数も関数もまとめる。それを再利用性のあるコードにする。カプセル化って表現もありますね。コンストラクター(Swiftだとイニシャライザ)に初期化のときに入れる値や実行するメソッドを書くこと。

Flutterのコードだとこんなのがある。シングルトンにしなくても良いらしいが...
https://ja.wikipedia.org/wiki/Singleton_パターン

スナックバーの多重継承をして、Widgetクラスで継承できるようにしたコード
import 'package:flutter/material.dart';

/// [シングルトンで、SnackBarを表示するためのクラス]
class SnackBarUtils {
  // singleton
  SnackBarUtils._();
  static final instance = SnackBarUtils._();

  void showErrorSnackBar(BuildContext context, String message) {
    final snackBar = SnackBar(
      backgroundColor: Colors.red,
      content: Text(message),
      duration: const Duration(seconds: 3),
    );
    ScaffoldMessenger.of(context).showSnackBar(snackBar);
  }
}

/* [mixinで、SnackBarを表示するためのクラス]
例えば、SignInPageで使う場合は、
class SignInPage extends HookConsumerWidget with SnackBarMixin
で多重継承できる。こうすることで、UI側でSnackBarを表示することがメソッドを
mixinクラスから呼ぶことができる。
*/
mixin SnackBarMixin {
  void showErrorSnackBar(BuildContext context, String message) {
    SnackBarUtils.instance.showErrorSnackBar(context, message);
  }
}

音楽プレーヤーのプログラムを再現するとこんな感じですか?

機能を実装していない、抽象クラスであるabstract classこれはTypeScriptでもあるけど、インターフェースとか呼ばれているものですね。Dart3.0からもインターフェースはあるけど...

解説が変だったので修正
すささんからご指摘がありました。

@override は親クラスののメソッドを上書きすることを指し、
依存性注入(DI)は、依存するインスタンスを外部から与える(注入する)ことを指すので、別物と認識しています。修正お願いします!

@overrideを使った例ですが、このような書き方をします。機能を実装してないインターフェースや抽象クラスのメソッドを上書きして、機能を実装します。今回は単純ですけどログを出す処理をつけました。いつもだったら、FirebaseAuthの認証機能をつけたり、JWT認証の処理を書いてますね。

// 親クラスというもの
abstract class MemberAuth {
  // 機能は実装していない
  void login();
  void logout();
}

// 親クラスを継承した子クラスというもの
class MemberAuthImpl implements MemberAuth {

  /* @overrideというアノテーションなる記号をつけると、
  親クラスのメソッドを上書きして、子クラスで実装することができる。ロジックを`{}`の中に書くということ。
  */
  
  void login() {
    // logだけで申し訳ない🙇
    print('ログインしました');
  }

  
  void logout() {
    print('ログアウトしました');
  }
}

使うときは、クラスをインスタンス化して、メソッド(関数)とプロパティ(変数)を呼ぶことができるので、使って音楽の再生ができる。

Dartのコード
// プロトコル(抽象クラス)の定義
abstract class MusicPlayerDelegate {
  void musicPlayerDidStartPlaying(MusicPlayer player);
  void musicPlayerDidStopPlaying(MusicPlayer player);
}

// 音楽プレーヤーの定義
class MusicPlayer {
  MusicPlayerDelegate? delegate;

  void playMusic() {
    // 音楽を再生するロジック...
    delegate?.musicPlayerDidStartPlaying(this);
  }

  void stopMusic() {
    // 音楽を停止するロジック...
    delegate?.musicPlayerDidStopPlaying(this);
  }
}

// デリゲートを実装したクラスの定義
class MusicPlayerController implements MusicPlayerDelegate {
  final player = MusicPlayer();

  MusicPlayerController() {
    player.delegate = this;
  }

  
  void musicPlayerDidStartPlaying(MusicPlayer player) {
    print('Music started playing.');
  }

  
  void musicPlayerDidStopPlaying(MusicPlayer player) {
    print('Music stopped playing.');
  }
}

// インスタンスの作成とメソッドの呼び出し
void main() {
  final controller = MusicPlayerController();
  controller.player.playMusic();
  controller.player.stopMusic();
}

TypeScriptでインターフェースを使った例だとこんな感じです。難しいこと書いてるでしょ。でもやってることは同じですよ。機能を実装していないインターフェースを実装して、使ってるだけ。Dartでもあるけど、constructorに初期化されたときに実行する処理や値を設定してるだけ。

TypeScriptの場合
// プロトコル(インターフェース)の定義
interface MusicPlayerDelegate {
  musicPlayerDidStartPlaying(player: MusicPlayer): void;
  musicPlayerDidStopPlaying(player: MusicPlayer): void;
}

// 音楽プレーヤーの定義
class MusicPlayer {
  delegate?: MusicPlayerDelegate;

  playMusic() {
      // 音楽を再生するロジック...
      if (this.delegate) {
          this.delegate.musicPlayerDidStartPlaying(this);
      }
  }

  stopMusic() {
      // 音楽を停止するロジック...
      if (this.delegate) {
          this.delegate.musicPlayerDidStopPlaying(this);
      }
  }
}

// デリゲートを実装したクラスの定義
class MusicPlayerController implements MusicPlayerDelegate {
  player: MusicPlayer;

  constructor() {
      this.player = new MusicPlayer();
      this.player.delegate = this;
  }

  musicPlayerDidStartPlaying(player: MusicPlayer) {
      console.log("Music started playing.");
  }

  musicPlayerDidStopPlaying(player: MusicPlayer) {
      console.log("Music stopped playing.");
  }
}

// インスタンスの作成とメソッドの呼び出し
let controller = new MusicPlayerController();
controller.player.playMusic();
controller.player.stopMusic();

DIとは?

参考になるサイトをみると、このように書いてますね。
https://ja.wikipedia.org/wiki/依存性の注入
依存性の注入(いぞんせいのちゅうにゅう、英: Dependency injection)とは、あるオブジェクトや関数が、依存する他のオブジェクトや関数を受け取るデザインパターンである。

簡単なコードを書いてみた

このコードでは、Serviceという抽象クラスを定義し、RealServiceとMockServiceという2つの具体クラスで実装しています。ClientクラスはServiceに依存しており、その依存性はコンストラクタ経由で注入されます。これにより、Clientは具体的なServiceの実装に依存せず、代わりに抽象クラスに依存します。これが依存性の注入の基本的な考え方です。

Dartのコードで表現すると単純なものならこんな感じですかね。依存性ってワードが分かりづらい。

abstract class Service {
  void performTask();
}

class RealService implements Service {
  void performTask() {
    print('Performing real service task');
  }
}

class MockService implements Service {
  void performTask() {
    print('Performing mock service task');
  }
}

class Client {
  final Service _service;

  Client(this._service);

  void performTask() {
    _service.performTask();
  }
}

void main() {
  // 本番環境
  Client realClient = Client(RealService());
  realClient.performTask();

  // テスト環境
  Client mockClient = Client(MockService());
  mockClient.performTask();
}

riverpodだったらテストコードを書くときに、このDIっていうのがよく出てきますね。テスト書くときだと、仮のデータ、モックを入れるので、注入ってよくいうんでしょうね。確かにこれだと、上書きではないな。元々データないですからね。外部(外)から値を与えてますね。注入ってのも慣れないと分かりづらい。

https://riverpod.dev/ja/docs/cookbooks/testing


test('override repositoryProvider', () async {
  final container = ProviderContainer(
    overrides: [
      // repositoryProvider の挙動をオーバーライドして
      // Repository の代わりに FakeRepository を戻り値とする
      repositoryProvider.overrideWithValue(FakeRepository())
      // `todoListProvider` はオーバーライドされた repositoryProvider を
      // 自動的に利用することになるため、オーバーライド不要
    ],
  );

  // 初期ステートが loading であることを確認
  expect(
    container.read(todoListProvider),
    const AsyncValue<List<Todo>>.loading(),
  );

  /// リクエストの結果が戻るのを待つ
  await container.read(todoListProvider.future);

  // 取得したデータを公開する
  expect(container.read(todoListProvider).value, [
    isA<Todo>()
        .having((s) => s.id, 'id', '42')
        .having((s) => s.label, 'label', 'Hello world')
        .having((s) => s.completed, 'completed', false),
  ]);
});

ビジネスロジックが分かれば、プログラミング言語は違っても書けるようになる。筆者は最近だと、SwiftとKotlinを趣味でやってますが、基本的なことが分かればプログラムだけなら書けるようになります。

わかりやすく言うなら難しいプログミング言語をやってたらわかる

PHPとRubyだと参考にならないかもしれない💦
Pythonもダメそう!!!!
基本クラスで書かなくても動く!、簡単な方だし、文法が似てないので参考にならないと思う。

やるなら、C++、C#、Java、クラスで書いてるJavaScripが良いと思う。概念が似ているから、理解はしやすくなる。クラスがあって継承があって、staticとか、private(Dartだと、_(アンダースコア))があるので、似ている。

C#だとこんな感じですね。Hello Worldするだけでこんなに書く💦

using System;

class Hello
{
    static void Main()
    {
        Console.WriteLine("Hello, World");
    }
}

Dartにしたらこんな感じかな?

void main() {
  final greet = Greet.hi();
}

class Greet {
  static void hi() {
    print("Hi");
  }
}

はじめてDartをやる人には、Dartの公式を読めば良いと僕は言いますね。でも日本の語の情報で、オブジェクト思考とか理解したいなら、昔からある言語の本を読んでみると良いと思います。
他には、文法を覚えてきたら自分が得意なプログミング言語に置き換えてみるとか?

私は昔プログミングの勉強してた時は、PHPとRubyをやってましたが、JavaScrptの方が書きやすかったので、よく書いてました。これはTypeScrptですが...

class Greet {
  userName: string;
  age: number;
  
  constructor() {
    this.userName = "Jboy";
    this.age = 34;
  }

  init() {
    console.log(`名前${this.userName}: 年齢${this.age}`);
  }
}

let greet = new Greet();
greet.init();

オブジェクト思考についてのまとめ

変数で値を保持する、関数に処理をまとめる。if文とswitch文で、エラー処理などの分岐処理をする。for文やwhile文で繰り返し処理をする。クラスに全部まとめて設計図にする。クラスはどこで呼び出すのか?
インスタンス化した場所である。

何かプログラムを考えるときに、もしDartで情報がないなら、SwiftやKotlin、TypeScriptを参考すると良いかもしれない。ソフトウェアエンジニアとお話しすると、よくエバンス本とか、ドメイン知識の本の話題が出てくる。

本を読むと良いですね。後は、勉強会に行ってみる。そうすると見えてくるものがある。

設計の勉強をするなら、こちらの本がおすすめです。フォルダの配置の仕方が理解できるようになると思う。
https://booth.pm/ja/items/1835632

Dartの公式はこちら💁‍♂️
https://dart.dev/

Dart触ってみたい人は本書いたので見てみてください。
https://zenn.dev/joo_hashi/books/34fa9e03ab3440

MVVMならこれが良い?、Viewが画面で、Modelがデータを保持する変数とか書いてある。RepositoryにAPIやDBを操作するロジックを書く。ViewModelは、ViewとModelの間にいて、Repositoryのコードを呼び出したり、ViewからModelに通知をして、データを更新したり、その逆のViewの更新もする。

https://developer.android.com/jetpack/guide?hl=ja

MVCをSwiftを参考にすべきか、Railsを参考にすべきか?
Modelは値を保持する変数があって、Viewが入力や表示画面で、その間にControllerがいて、変更を通知してくれるものですね。Railsでも一緒かな〜

https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html

目的に応じて、フォルダごとにファイルを分ければ良いと言うことです。

🔺ライフサイクルを理解する.

Webのエンジニアだと掴みにくいかも?
PHPとRubyにライフサイクルあるかな...、ReactとVue.jsをやってればピンとくると思う。画面がレンリングされる、画面が呼ばれたら実行される処理がある、ホットリロードで画面が更新される。

initStateについて

画面が呼ばれると、最初に実行される処理を書いておく。それがDBの接続だったり、アニメーションの実行だったり、ユースケース(シナリオ)はさまざまです。これはStatefulWidgetクラスでしか使えないですけどね。

https://api.flutter.dev/flutter/widgets/State/initState.html

class HomePage extends StatefulWidget {
  
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late Realm realm;// Realmインスタンスを保持するための変数
  final _nameController = TextEditingController();
  final _personsController = StreamController<List<Person>>.broadcast();

  
  void initState() {
    super.initState();
    final config = Configuration.local([Person.schema]);
    realm = Realm(config);
    _fetchPersons();
  }

  
  void dispose() {
    _nameController.dispose();
    _personsController.close();
    super.dispose();
  }

  // _fetchPersonsは、RealmからPersonのリストを取得し、_personsControllerに追加するメソッド
  void _fetchPersons() {
    final persons = realm.all<Person>().toList();
    _personsController.add(persons);
  }

  
  Widget build(BuildContext context) {

Swiftだとini()メソッドがある。

// クラスの定義
class SampleClass {
    var name: String
    var age: Int

    // 初期化メソッド
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

Kotlinでもinitはある。

class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println)
    
    init {
        println("First initializer block that prints $name")
    }
    
    val secondProperty = "Second property: ${name.length}".also(::println)
    
    init {
        println("Second initializer block that prints ${name.length}")
    }
}

Reactだと、useEffectを使う。ページが呼ばれた時に実行される。

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);
  // ...
}

Vue.jsの3系だとこれに変わってるみたいだな。Vue2.6と違う💦

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  console.log(`コンポーネントがマウントされました。`)
})
</script>

addPostFrameCallbackというのもある

これは、initStateの後にコールバック関数を実行する時に使います。呼ばるタイミングはフレームの描画が終わった後ですね。終わった後ってことは遅延があるってことですね。待ってから実行される。
initStateの中で、contextを参照したい時に最近私は使う機会がありましたね。

https://zenn.dev/jboy_blog/articles/211dabe9e0b4d2

setStateとは?

状態を管理するものですね。呼ばれるタイミングは、voidCallbackを実行したときですね。難しい表現ですね。ボタンを押したときに、実行される例がわかりやすいかな。カウンターアプリがわかりやすいですよ。ボタンを押すたびに、callbackの中で、メソッドが実行されて、setStateが呼ばれる。足し算が繰り返されて数字は増える。画面が更新されるので、増えていくのが画面に表示されている。

Flutterのデフォルトのカウンターアプリ
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const MaterialApp(title: 'Flutter Demo', home: CounterWidget());
  }
}

class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});

  
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('Count: $_counter'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Flutterのライフサイクルは、Reactの影響を受けているという。確かにsetStateuseStateに似ている。countがプロパティを保持していて、setCountがコールバックの中で実行されるとState(状態)を更新する。そうすると、画面が更新されて足し算がされて数字が増えていく現象が起きる。

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      You pressed me {count} times
    </button>
  );
}

dispose method

状態を破棄するときに使うメソッドです。StatefulWidgetでしか使わないですね。riverpodだとautoDisposeがあります。onDisposeというものもありますね。
https://api.flutter.dev/flutter/widgets/State/dispose.html
TextEditingControllerの状態を破棄しないと、不要なメモリが蓄積されてメモリリークが起きてしまう。

メモリリーク(memory leak)とはプログラムのバグの一つで、使用していないメモリを開放することなく確保し続けてしまう現象のこと。 leakは直訳で「漏れ出す」の意味。 通常ソフトウェアなどは動作にあたって必要なメモリを確保し、使用後は解放する。

メモリリークするとどうなる?
メモリリークとは、プログラムが動的にメモリを割り当てたあと、メモリが適切に開放されない場合に発生します。 メモリリークによって、メモリが不足すると、アプリケーションの動作が不安定になったり、クラッシュやフリーズを引き起こすことがあります。

なので使わないものは、解放してあげましょう!、破棄すると言った方が良いか...

class _HomePageState extends State<HomePage> {
  late Realm realm;// Realmインスタンスを保持するための変数
  final _nameController = TextEditingController();
  final _personsController = StreamController<List<Person>>.broadcast();

  
  void initState() {
    super.initState();
    final config = Configuration.local([Person.schema]);
    realm = Realm(config);
    _fetchPersons();
  }

  
  void dispose() {
    _nameController.dispose();
    _personsController.close();
    super.dispose();
  }

https://riverpod.dev/ja/docs/concepts/provider_lifecycles
プロバイダーが破棄されるときに何らかのアクションを実行する必要がある場合は、ref の onDispose メソッドを使用してコールバックを登録します。 次の例では、onDispose を使用して StreamController を閉じます。


Stream<int> example(ExampleRef ref) {
  final streamController = StreamController<int>();

  ref.onDispose(() {
    // Closes the StreamController when the state of this provider is destroyed.
    streamController.close();
  });

  return streamController.stream;
}

まとめ

長い記事になって読むと疲れましたね。読んでくれてありがとう〜

わかって欲しいことは2個だけ。

1個目は:
仕様書通りにプログラムを書きたいので、ビジネスロジックを作る。そのためには、Objet思考を理解する。クラスがあって、メソッドがあって、プロパティがある。Objetってのは、データの集まりとか表現されてます。テトリスを作るのは無理かもしれないが...

2個目は:
ライフサイクルを理解すること。モバイルをやってる人はピンとくると思います。ローカルDBのデータを更新したのに、前のページに戻ってもデータが更新されてない?
リビルド(ビルドし直す)すると、値が変わってる。次のページに値を渡すには、コンストラクターに引数を渡す必要がある。画面遷移の時に、値を引数で渡す。ページが呼ばれたらアニメーションやAPIのデータを表示したい。
何か処理を実行したい。その処理ってどのタイミングで呼ばれるのか?、ビルドしたタイミングかページが呼ばれた時か???

最近、知り合いの人と一緒に仕事をして、気取ったコードを書くと「これはシングルトンで書く必要がない」と言われました。
僕は普段は、riverpod generatorとかHookWidgetを使ってるので、Dartの文法でmixinでUIを作るクラスに多重継承したいとかやりません。Dart3のEnumはswitch式で書けるので、ロジックを作ってみたりしたけど、デザインパターンなるものを考えてないといけないと思った。
普段使わないものを思いつきで使うと、エンジニア長い人にはこれはいらないと突っ込まれる💦

riverpodgo routerは技術に強い企業では使っているから使えないと死ねと思われるので、モダンな技術ばかりやって、Dartのオブジェクト思考とデザインパターンを今まで考えたことなかったですね。SwiftとKotlinを勉強すると必要なことが多かったので、Dartでも設計パターンを意識したいですね。

オブジェクト思考が分かっていれば、riverpodを使わなくてもスナックバーをView側でエラーが出たら出す処理をいけてるコードで書けたりする。ref.listen使わなくてもできる。withでWidgetのクラスに多重継承すれば、mixinからメソッドを呼び出すことができる。

僕の友達たちは、正社員で働いていたり、講師をやってるけど、ソフトウェアを作ってる会社の人はもっとすごくて、オフセットがどうとか?、色の指定は0xなんとかを変える?、typedefでやるだのコンピューターの知識や他の技術の経験から、なんかすごいロジックを作ったり、設計はどう学べば良いのか教えてくれました。

他にもSwiftとKotlinができて、Spring BootとSQLができる人は、Sierの仕事をした時に、1日でTypeScripのUnit Testを理解したみたい?
テーブル定義やSQLも見直していた。そんな人は、FlutterのライフサイクルやDartの文法をすぐ理解していた。

すご〜く長くなったけど、「仕組み」を理解すれば他の技術からヒントを得て、ロジック考えたり新しいパッケージを触っても使いこなせるようになることを私は伝えたかった。

FirebaseやAPIなど外部サービスを使うのはコードを書くのとは、別の分野なのでそちらは別の設計の知識とかが必要になってくる。これができるようになるには、ドキュメントや技術記事を読む必要がある。

ということで、みなさん本を読んでください笑、あとはコードを書いて研究して専門家と話せば、理解できるようになります。Flutterは簡単な方です。複雑なことをしなければ簡単です。

Discussion

すさすさ

@overrideは依存性注入なんですか?

JboyHashimotoJboyHashimoto

う〜ん、上書きしてるという解釈ならそうらしいですが、僕も疑問ですね。
ここは修正しておきます笑

すさすさ

@override は親クラスののメソッドを上書きすることを指し、
依存性注入(DI)は、依存するインスタンスを外部から与える(注入する)ことを指すので、別物と認識しています。修正お願いします!