Flutter を使ってみた

# はじめに

これまでモバイルアプリ開発するのに、HTML＋JavaScript＋Cordova を使ってきました。
先日ある勉強会で Flutter に触れました。この機会に使えるようにしておきたいと思いました。

以前に書いていた記事 [Onsen UI＋Vue.js を使ってみた](https://qiita.com/drafts/82cce234e4b6fc0f17c7) を Flutter を使って書き直してみました。

# Flutter を使ってみた

* Flutter 1.17.5
* Dart 2.8.4

## Flutter を使う

以下のページからダウンロードしてインストールします。

Flutter
(https://flutter.dev/)

## 最初のアプリ

初期画面を表示するだけのアプリです。

```dart:main.dart
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Try Flutter',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Try Flutter'),
      ),
      body: Center(
        child: Text('Hello Flutter'),
      ),
    );
  }
}
```

## データを準備する

まず、データを準備します。「連絡先」の配列にします。

```dart:main.dart
class Contact {
  String firstName;
  String lastName;
  bool hasPhone;
  String phoneNumber;

  Contact(firstName, lastName, hasPhone, phoneNumber) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.hasPhone = hasPhone;
    this.phoneNumber = phoneNumber;
  }
}

final List<Contact> contacts = [
  Contact('Alice', 'Arten', true, '555-0184'),
  Contact('Bob', 'Brigham', false, '555-0163'),
  Contact('Charlie', 'Campbell', true, '555-0129'),
];
```

## データをレンダリングする

画面にリストビューを用意してデータを表示します。

```dart:main.dart
class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ContactList(),
```
```dart:main.dart
class ContactList extends StatefulWidget {
  @override
  _ContactListState createState() => _ContactListState();
}

class _ContactListState extends State<ContactList> {
  @override
  Widget build(BuildContext context) {
    return _listView();
  }

  Widget _listView() {
    return ListView.builder(
      itemBuilder: (BuildContext context, int i) {
        return _listItem(contacts[i]);
      },
      itemCount: contacts.length,
    );
  }

  Widget _listItem(Contact _contact) {
    return ListTile(
      title: Text('${_contact.firstName} ${_contact.lastName}'),
    );
  }
}
```

## ユーザ入力をハンドリングする

リストビューに「編集」と「削除」ボタンを追加します。```onPressed``` に実行する処理を記述します。

```dart:main.dart
  Widget _listItem(Contact _contact) {
    return ListTile(
      title: Text('${_contact.firstName} ${_contact.lastName}'),
      trailing: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          IconButton(
            icon: Icon(Icons.edit),
            onPressed: () {
              debugPrint('showDetail');
            }
          ),
          IconButton(
            icon: Icon(Icons.delete),
            onPressed: () {
              debugPrint('deleteItem');
            }
          ),
        ],
      ),
    );
  }
```
## データを削除できるようにする

データを操作するコードを追加します。

```dart:main.dart
  Widget _listItem(Contact _contact) {
  中略
          IconButton(
            icon: Icon(Icons.delete),
            onPressed: () {
              contacts.remove(_contact);
            }
          ),
```

データを操作しても画面の表示は変わりません。```setState()``` を追加します。

```dart:main.dart
          IconButton(
            icon: Icon(Icons.delete),
            onPressed: () {
              setState(() {
                contacts.remove(_contact);
              });
            }
          ),
```

## 詳細ページを表示する

まず、詳細ページを用意します。↓

```dart:main.dart
class MySecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Page'),
      ),
      body: ContactDetail(),
    );
  }
}

class ContactDetail extends StatefulWidget {
  @override
  _ContactDetailState createState() => _ContactDetailState();
}

class _ContactDetailState extends State<ContactDetail> {
  final _firstName = TextEditingController();
  final _lastName = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(8.0),
      child: Column(
        children: [
          TextFormField(
            decoration: InputDecoration(hintText: 'First Name'),
            controller: _firstName,
          ),
          TextFormField(
            decoration: InputDecoration(hintText: 'Last Name'),
            controller: _lastName,
          ),
        ]
      )
    );
  }
}
```

```Navigator.pop()``` で新規画面を開きます。↓

```dart:main.dart
class _ContactListState extends State<ContactList> {
中略
          IconButton(
            icon: Icon(Icons.edit),
            onPressed: () {
              Navigator.push(context,
                MaterialPageRoute(
                  builder: (context) => MySecondPage(),
                )
              );
            }
          ),
```

## 詳細ページに内容を表示する

```Navigator.push()``` するとき値を渡して、```ModalRoute.of()``` で受取ります。

```dart:main.dart
class _ContactListState extends State<ContactList> {
中略
              Navigator.push(context,
                MaterialPageRoute(
                  builder: (context) => MySecondPage(),
                  settings: RouteSettings(
                    arguments: _contact,
                  )
                )
              );

class _ContactDetailState extends State<ContactDetail> {
  final _firstName = TextEditingController();
  final _lastName = TextEditingController();

  @override
  Widget build(BuildContext context) {
    final Contact _contact = ModalRoute.of(context).settings.arguments;
    _firstName.text = _contact.firstName;
    _lastName.text = _contact.lastName;
```

## 詳細画面で変更した内容で一覧表示を更新する

```Navigator.pop()``` するとき値を返して、```Navigator.push()``` のコールバックで受取ります。

```dart:main.dart
class _ContactDetailState extends State<ContactDetail> {
中略
          RaisedButton(
            child: Text('Update'),
            onPressed: () {
              _contact.firstName = _firstName.text;
              _contact.lastName = _lastName.text;
              Navigator.pop(context,
                _contact
              );
            },
          ),

class _ContactListState extends State<ContactList> {
中略
              Navigator.push(context,
              中略
              )
              .then((result) {
                setState(() {
                  _contact = result;
                });
              });
```

## 詳細ページに内容を表示する別のやり方

変数 ```selected``` を用意して使います。

```dart:main.dart
Contact selected;
```

```RouteObserver<PageRoute>()``` を使って、```didPush()``` と ```didPop()``` を用意します。

```dart.main.dart
final myRouteObserver = new RouteObserver<PageRoute>();

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
      navigatorObservers: <NavigatorObserver>[myRouteObserver],
      中略

class _ContactDetailState extends State<ContactDetail> with RouteAware {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    myRouteObserver.subscribe(this, ModalRoute.of(context));
  }
  @override
  void dispose() {
    myRouteObserver.unsubscribe(this);
    super.dispose();
  }
  void didPush() {
    // この画面が push() されてきたときに実行される
  }
  void didPop() {
    // この画面が pop() されるときに実行される
  }
```

```Navigator.push()``` する前に selected に値をセットして、```didPush()``` で受取ります。

```dart:main.dart
class _ContactListState extends State<ContactList> {
中略
              selected = _contact;
              Navigator.push(context,
                MaterialPageRoute(
                  builder: (context) => MySecondPage(),
                )
              );

class _ContactDetailState extends State<ContactDetail> with RouteAware {
  中略
  void didPush() {
    _firstName.text = selected.firstName;
    _lastName.text = selected.lastName;
  }
```

## 詳細画面で変更した内容で一覧表示を更新する別のやり方

```RouteObserver<PageRoute>()``` を使って、一覧ページに ```didPopNext()``` を用意します。

```dart.main.dart
final myRouteObserver = new RouteObserver<PageRoute>();

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
      navigatorObservers: <NavigatorObserver>[myRouteObserver],
      中略

class _ContactListState extends State<ContactList> with RouteAware {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    myRouteObserver.subscribe(this, ModalRoute.of(context));
  }
  @override
  void dispose() {
    myRouteObserver.unsubscribe(this);
    super.dispose();
  }
  void didPopNext() {
    // 上の画面が pop() されて戻ってきたとき実行される
  }
```

```selected``` はポインタなので、詳細ページで値がセットされた時点で ```contancts``` が更新されます。
データを更新しても画面が更新されないので、一覧ページの ```didPopNext()``` で ```setState()``` します。

```dart:main.dart
class _ContactDetailState extends State<ContactDetail> with RouteAware {
  中略
  void didPop() {
    selected.firstName = _firstName.text;
    selected.lastName = _lastName.text;
  }

class _ContactListState extends State<ContactList> with RouteAware {
  中略
  void didPopNext() {
    setState(() {
    });
  }
```

## データを追加できるようにする

「追加」ボタンを追加します。

```dart:main.dart
class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      中略
      floatingActionButton: AddContact(),
    );
  }
}

class AddContact extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      child: Icon(Icons.add),
    );
  }
}
```

「追加」ボタンの処理を追記します。

```dart:main.dart
    return FloatingActionButton(
      中略
      onPressed: () {
        final _contact = Contact('', '', false, '');
        contacts.add(_contact);
        selected = _contact;
        Navigator.push(context,
          MaterialPageRoute(
            builder: (context) => MySecondPage(),
          )
        );
      },
    );
```