😉

Flutterちゃんとなかよくなりたくて(by PHPエンジニア)

2024/12/24に公開

Flutter先生ずっとファンでした。PHPエンジニアですけど仲良くしてください。

初めに

こんにちは!今回はFlutterちゃんを勉強して使って軽くアプリを一つ作ってみようと思います。
目標としては以下になります。

Dart言語、Flutterを学んでシンプルなアプリを一つ作ってみる

Dart言語に関して

flutterとは

スマートフォンのアプリケーション開発に特化したモバイルフレームワークの一種です。
特徴としては以下になります。

  • iOSとAndroidのアプリを一度に開発できる
  • 独特のプログラミング言語(Dart)を使用する
    :DartはGoogleによって開発されたプログラミング言語ですが、世界的に人気の高い言語でお馴染みの「JavaScript」の設計を踏襲して作られています。
  • ホットリロード(HotReload)機能 : プログラムの変更を即座にUIへ反映する機能

なるほど、flutterを使いたいんだったらDart言語をある程度把握しないと行けないことをわかりました。

Dart言語

Flutterとかinstallしなくても以下のURLでDartのコードを試すことができるので勉強は以下のサイト上でコードを回すことにします。
https://dartpad.dev/

変数(Variables)

javascriptで書くみたいにvarを書いたら変数を定義することが可能になります。

  var name = 'hoge';

PHPと違く、定義時とデータ型を合わせないとエラーになるということでした。

var name = 'hoge';
name = 1; // ng

main.dart:3:10: Error: A value of type 'int' can't be assigned to a variable of type 'String'. name = 1; // ng

varは慣習的にmethodのなかかlocalの変数を定義する時に使うという用途でした。
compilerはデータ型を知っていて別に書かなくてもいいみたいです。

明示的に書いても定義ができます。
こちらの場合はクラスのpropertyを定義する用途で使うということでした。

Strings name = 'hoge';

PHPみたいに動的にタイプが変わるようにも定義する方法はありました。
しかしtypeの判別をして該当変数を使う必要があります。

dynamic name;
if (name is String) {
}

Nullableのものは以下みたいに表現しないとString -> Nullとして利用された時にエラー

Strings? name = 'hoge';

Data types

Dartの変数は全てobjectです。importとかしなくて変数の .hogehogeみたいに書くとobjectとしてデータ型Objectの機能を使えます。

int number = 1;
/// * [Numbers](https://dart.dev/guides/language/numbers) in
/// [A tour of the Dart language](https://dart.dev/guides/language/language-tour).
abstract final class int extends num {

List型の場合もobjectとして利用可能です。

void main() {
  List list = [1, 2, 3, 4];
  print(list.first);
  print(list.last);
  print(list.length);
  list.add(5);
  print(list.last);
}

存在するかしないかのものも一応かListの定義時にif文と一緒に書いて宣言できるということでした。

void main() {
  var giveMeFive = true;
  List list = [
    1,
    2,
    3,
    if (giveMeFive) 5,
  ];
  print(list);
}
result
  [1, 2, 3, 5]

テキスト結合する時 「''」 , 「""」の中に 「$hoge」みたいに変数をかけばいいということと
「${hoge + 1}」みたいに書いたら数式も計算の結果で結合できます。
文字列結合時にescapeする時には前の文字一つ「\」にすればできます。

void main() {
  var name = 'hoge';
  var age = 29;
  var greeting = "Hello Everyone, my name is $name, I\'m${age + 1}";
  print(greeting);
}
result
Hello Everyone, my name is hoge, 30

既存のcollectionと合わせる技もありました。
書き方が柔軟ですね

void main() {
  var oldFriends = ['tanaka', 'sakura'];
  var newFriends = [
    'lewis',
    'jone',
    for(var friend in oldFriends) "$friend",
  ];
  print(newFriends);
}
result
[lewis, jone, tanaka, sakura]

Mapは以下みたいに定義できます。
Mapの中にMap入れるのも可能だった無限にMapの中にMap入れるのでは?
少し変わった作りもできそうですね

void main() {
  Map<int, bool> player = {
    1: true,
    2: false,
    3: true
  };
  Map<List<int>, bool> player2 = {
    [1,2,3,4,5]: true,
  }; 
  Map<Map<int, bool>, bool> player3 = {
    player: true,
  };
}

Functions

functionは以下みたいに定義できます。返すreturnのデータタイプを頭に書いて定義する形式
=>を書いて1行で定義するとそれをすぐ返すものでした。
直感的で何をreturnするのかすぐわかって嬉しいです。

String sayHello(String potato) {
  return "Hello $potato nice to meet you!";
}
String sayHello2(String potato) => "Hello $potato nice to meet you!";

Dartではnamed parameterという機能がサポートされるんですが、以下みたいなことができます。
function定義時に「{}」で囲むことでパラメータ名と値を指定してfunctionを使えます。
順番を守らなくても柔軟にまた直感的に書けるからわかりやすいし順番ミスによる不具合も減るかもですね

String sayHello({String potato, int age}) {
  return "Hello $potato nice to meet you! I\'m $age ";
}

void main() {
  print(sayHello(
    age: 12,
    name:hoge,
  ));
}

Classes

クラスの定義方法は他の言語と同じく以下のように書けます。
特別な点は nameだと書くだけで{this.name}とかで書かなくても動いてくれるみたいです。

class Player {
  int xp = 1500;
  void sayHello() {
    print("Hi my name is $name");
  }
}

constructorもシンプルに書けます

class Player {
  String name;
  int xp;
  Player(this.name, this.xp)
}

named parameter機能ももちろん使えます。

Player({this.name, this.xp})

extendsは以下のようにできる
phpではparent::とかで書いてた気がするけどこっちではsuperと書く感じでした。

class Television {
  void turnOn() {
    _illuminateDisplay();
    _activateIrSensor();
  }
  // ···
}

class SmartTelevision extends Television {
  void turnOn() {
    super.turnOn();
    _bootNetworkInterface();
    _initializeMemory();
    _upgradeApps();
  }
  // ···
}

--

シンプルなアプリを作ってみよう!

install

flutterを書くために調べたら xcode, android studioのinstallがまず必要でした。
また jdk, android studioのsdkをインストールの方もinstallとパスを通す必要がありました。

flutter docorが通れば無事にセットアップ終わったと言う感じです
叩きながら一応完了。

要件

お昼何食べるかを近所のお店で検索してくれるものをFlutterで作る

設計

画面的には2つ構成で行きます。
1つ目:入力フォーム
2つ目:検索結果画面

入力フォームでお昼の自分の位置、予算、歩いて何分距離、好き嫌いの食べ物を設定します。
入力してもらったものを元にpromptを作成して gemini apiを利用して結果画面を構成して見せます。

入力フォームsubmitボタンを作ってみよう

widgetというものを利用すればウェブのhtmlみたいに画面に出る入力フォーム、ボタンなどが配置できるみたいです。
https://qiita.com/matsukatsu/items/e289e30231fffb1e4502

今回はテキスト入力、ドロップダウン、submitボタンが必要なので検索して探しながら入れました。

テキスト入力フォーム

            TextField(
              controller: _foodPreferencesController,
              decoration: InputDecoration(
                labelText: '好きな食べ物',
                border: OutlineInputBorder(),
              ),
            ),

onchangedイベントを利用して選択した値を変数に入れました。

            DropdownButtonFormField<String>(
              decoration: InputDecoration(
                labelText: '歩いて何分距離',
                border: OutlineInputBorder(),
              ),
              items: <String>[
                '5分以内',
                '10分以内',
                '15分以内',
                '20分以内',
              ].map<DropdownMenuItem<String>>((String value) {
                return DropdownMenuItem<String>(
                  value: value,
                  child: Text(value),
                );
              }).toList(),
              value: _selectedDistance,
              onChanged: (String? newValue) {
                setState(() {
                  _selectedDistance = newValue;
                });
              },
            ),

submitボタン

submitボタンにonPressedイベントを利用して押下時に
widget,logicで定義したResultPage Classの画面表示、処理にRouteするようにしました。
また入力値をすべて引数として渡せるようにしました。(named parameters利用)

            ElevatedButton(
              onPressed: () {

                // 結果画面に移動으로 이동
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => ResultPage(
                      location: _locationController.text,
                      budget: _budgetController.text,
                      distance: _selectedDistance!,
                      foodPreferences: _foodPreferencesController.text,
                    ),
                  ),
                );
              },
              child: Text('Submit'),
            ),

結果画面を作ろう

結果画面に見せる情報は入力フォームからもらった情報を利用して
gemini apiと通信した結果をJSON配列でもらって処理を実施します。

final requestBody = {
      "contents": [
        {
          "parts": [
            {
              "text": "私は今 ${widget.location} にいます. お昼を食べれるいいお店を探しています。予算は ${widget.budget} 円ぐらいです. あるいて${widget.distance} の距離のランチが食べれるお店を1つ紹介してください。すきな食べ物は${widget.foodPreferences} です。答えは description: お店の名前と距離とおすすめメニュー, search_url: お店の名前をgoogleに検索する場合のURLのJSON配列のみでお願いします。"
            }
          ]
        }
      ]
    };

flutterのAPI通信のためにはpubspec.yamlhttpを追加してインストール?する必要があるみたいでやってみました。flutter pub getをterminalで叩くと入れられた

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.2 <追加した行

非同期処理

https://zenn.dev/iwaku/articles/2020-12-29-iwaku

  Future<void> fetchData() async {
    String apiKey = 'USE YOUR KEY'; 

    final requestBody = {
      "contents": [
        {
          "parts": [
            {
              "text": "私は今 ${widget.location} にいます. お昼を食べれるいいお店を探しています。予算は ${widget.budget} 円ぐらいです. あるいて${widget.distance} の距離のランチが食べれるお店を1つ紹介してください。すきな食べ物は${widget.foodPreferences} です。答えは description: お店の名前と距離とおすすめメニュー, search_url: お店の名前をgoogleに検索する場合のURLのJSON配列のみでお願いします。"
            }
          ]
        }
      ]
    };

    try {
      final response = await http.post(
        Uri.parse('https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent?key=$apiKey'),
        headers: {'Content-Type': 'application/json'},
        body: json.encode(requestBody),
      );

      if (response.statusCode == 200) {
        final jsonResponse = json.decode(response.body);

JSONをのレスポンスから必要な部分だけ取得

      if (response.statusCode == 200) {
        final jsonResponse = json.decode(response.body);
        setState(() {
          result = jsonEncode(jsonResponse);
          print(result);
          // 1. Mapで parsing
          final Map<String, dynamic> jsonData = jsonResponse;

          // 2. 'candidates' keyを利用して中身取る
          final List<dynamic> candidates = jsonData['candidates'];

          // 3. 巡回しながら必要な情報を変数に格納
          for (var candidate in candidates) {
            final Map<String, dynamic> content = candidate['content'];
            final List<dynamic> parts = content['parts'];
            final String text = parts[0]['text'];

            // 4. 'text' AIの返事のJSON配列から値取得
            final startIndex = text.indexOf('[');
            final endIndex = text.lastIndexOf(']') + 1;
            final extractedJsonString = text.substring(startIndex, endIndex);
            final List<dynamic> extractedJsonData = jsonDecode(
                extractedJsonString);

            // 5.JSON配列から'description'と'search_url'抽出
            for (var item in extractedJsonData) {
              description = item['description'];
              searchUrl = item['search_url'];
            }
          }
        });
      }

取得した情報を利用してお昼のおすすめのところの説明をテキストで表示、URLはボタンに配置させて飛べるようにしました。

  // Add a method to open the URL
  void _launchURL() async {
    final Uri url = Uri.parse(searchUrl);
    if (await canLaunchUrl(url)) {
      await launchUrl(url, mode: LaunchMode.externalApplication);
    } else {
      // Handle the error if the URL can't be launched
      setState(() {
        error = 'Could not launch $searchUrl';
      });
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('結果'),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Center(
          child: result != null
              ? Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SingleChildScrollView(child: Text(description)),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: _launchURL,
                child: Text('Open in Browser'),
              ),
            ],
          )
              : Text(error.isEmpty ? 'Loading...' : error),
        ),
      ),
    );
  }
}

urlを開くためにまたyamlに以下を書いてコマンドを実行する必要があった
なぜか壊れて一回クリーンしました。

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.2
  url_launcher: ^6.1.7 <これ追加

治してくれた魔法のおまじない🪄

flutter clean
flutter pub get
flutter run

結果物

https://www.youtube.com/shorts/hPdbwrzovE4

まとめ

・named parameterを使うと直感的ですごい嬉しかった
・classで画面単位の作業をまとめてるイメージ、widgetとロジックをうまく切り分けたい
・PHPエンジニアだからタイプ固定的だと慣れないと思ったがむしろ決まってるとすごい楽
・リアルタイムで保村したコードでアプリの挙動みれるのすごい楽(ホットリロード)
・flutterちゃんと少しは仲良くなれた気がする

GMOメディアテックブログ

Discussion