「筋トレ ビフォーアフター」を元にFlutterと個人開発を学ぶ

会社勤めをしながら個人開発をしている[takashi](https://twitter.com/cloverkizuna)です。

本記事では、Flutter製アプリ「筋トレビフォーアフター」を元にプロダクト開発の解説をしていきます。

長文になりますが、重要な部分を絞り込み、コードも記事用のサンプルコードに書き換えているので記事を読み進めやすいと思います。

アプリはこちら。無料でダウンロードできます。

![icon-60@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/c473465a-a3e5-2277-667f-9fbbdd43f7a7.png)

[<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/fc7d8fee-ebbe-1b9b-cdff-8d4eb0174a5d.png">](https://apps.apple.com/jp/app/id1470131371)

[<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/d44d1ddd-655e-08d7-3bb8-2291c0ee0218.png">](https://play.google.com/store/apps/details?id=com.miraku.MuscleBeforeAfter)

#目次
[1.Flutterを採用した理由](#1-flutterを採用した理由)
[2.Flutterのキャッチアップ](#2-flutterのキャッチアップ)
[3.設計](#3-設計)
[4.開発期間](#4-開発期間)
[5.アプリの規模感](#5-アプリの規模感)
[6.初回起動時の画面説明](#6-初回起動時の画面説明)
[7.入力画面](#7-入力画面)
[8.入力ダイアログ](#8-入力ダイアログ)
[9.カレンダー管理](#9-カレンダー管理)
[10.写真撮影とカメラライブラリ選択](#10-写真撮影とカメラライブラリ選択)
[11.データ登録完了時](#11-データ登録完了時)
[12.ビフォーアフター比較画面](#12-ビフォーアフター比較画面)
[13.ビフォーアフターのロック](#13-ビフォーアフターのロック)
[14.ビフォーアフターの履歴追加時](#14-ビフォーアフターの履歴追加時)
[15.ビフォーアフターの履歴一覧画面](#15-ビフォーアフターの履歴一覧画面)
[16.ビフォーアフターの履歴のシェア](#16-ビフォーアフターの履歴のシェア)
[17.写真管理画面](#17-写真管理画面)
[18.写真拡大表示](#18-写真拡大表示)
[19.グラフ表示画面](#19-グラフ表示画面)
[20.設定画面](#20-設定画面)
[21.バックアップ機能](#21-バックアップ機能)
[22.パスロック機能](#22-パスロック機能)
[23.このアプリを作った理由](#23-このアプリを作った理由)
[24.インフルエンサーマーケティング](#24-インフルエンサーマーケティング)
[25.マネタイズ](#25-マネタイズ)
[26.まとめ](#26-まとめ)

#1. Flutterを採用した理由
一番大きな理由はGoogleの優れたフレームワークによりiOS・Android両対応アプリを作れるからです。

私はiOSアプリを作った後にAndroidアプリでほぼ完全再現したことがありますが以下の問題を感じました。
・別OSでも同じようなロジックを書く必要がある
・OS固有のロジックを書く必要も出てくる
・レイアウトの仕組みがiOSとAndroidで異なる
・両方作るには開発工数だけでなくテスト工数もかかる
・個人開発だとアプリの同時リリースができない

そして、以下の理由でFlutterを採用しました。
・Flutterであれば開発工数やテスト工数を抑えられ、保守が楽になる
・キャッチアップの幅を広げすぎる必要がない
・iOSアプリでもマテリアルデザインが使える(Androidアプリを開発した時にマテリアルデザインを気に入っていた)
・Googleへの信頼度合いが強い

#2. Flutterのキャッチアップ

Flutterは公式ドキュメントがしっかりしています。公式ドキュメントを見ておくと開発がしやすくなります。
[Flutter公式](https://flutter.dev)

monoさんの記事は質が高いので、Flutterでアプリ開発をしたい方は必見です。
[monoさん記事](https://medium.com/flutter-jp/faq-f885a6494a8)

Flutterの書籍は4冊読みました。
・[基礎から学ぶ Flutter](https://www.amazon.co.jp/基礎から学ぶ-Flutter-石井幸次-ebook/dp/B082XQ81FH/ref=sr_1_1?__mk_ja_JP=カタカナ&dchild=1&keywords=Flutter&qid=1586432343&sr=8-1)
・[Android/iOSクロス開発フレームワーク Flutter入門](https://www.amazon.co.jp/Android-iOSクロス開発フレームワーク-Flutter入門-掌田津耶乃/dp/4798055832/ref=sr_1_2?__mk_ja_JP=カタカナ&dchild=1&keywords=Flutter&qid=1586432343&sr=8-2)
・[Flutter モバイルアプリ開発バイブル](https://www.amazon.co.jp/Flutter-モバイルアプリ開発バイブル-南里勇気/dp/4839970874/ref=pd_bxgy_img_3/358-7127009-1192537?_encoding=UTF8&pd_rd_i=4839970874&pd_rd_r=e077d8fe-4ce8-4c46-a85b-66ff83adc9f7&pd_rd_w=5ngjU&pd_rd_wg=R6GDP&pf_rd_p=3ed3024e-aea2-49a7-ac95-7612edc065d7&pf_rd_r=662BYYA5VMZ1KSMTNE0R&psc=1&refRID=662BYYA5VMZ1KSMTNE0R)
・[Flutter×Firebaseで始めるモバイルアプリ開発](https://www.amazon.co.jp/Flutter×Firebaseで始めるモバイルアプリ開発-技術の泉シリーズ（NextPublishing）-下畑-翔/dp/4844398784/ref=pd_bxgy_img_2/358-7127009-1192537?_encoding=UTF8&pd_rd_i=4844398784&pd_rd_r=e077d8fe-4ce8-4c46-a85b-66ff83adc9f7&pd_rd_w=5ngjU&pd_rd_wg=R6GDP&pf_rd_p=3ed3024e-aea2-49a7-ac95-7612edc065d7&pf_rd_r=662BYYA5VMZ1KSMTNE0R&psc=1&refRID=662BYYA5VMZ1KSMTNE0R)

個人的に気に入った書籍(実用向けだと感じた書籍)は「基礎から学ぶ Flutter」です。

動画でFlutterの学習を始めるのであれば以下が良いと思います。
[The Complete 2020 Flutter Development Bootcamp with Dart](https://www.udemy.com/course/flutter-bootcamp-with-dart/?utm_source=adwords&utm_medium=udemyads&utm_campaign=LongTail_la.EN_cc.ROW&utm_content=deal4584&utm_term=_._ag_77879424134_._ad_386759469016_._kw__._de_c_._dm__._pl__._ti_dsa-1007766171312_._li_1009278_._pd__._&matchtype=b&gclid=Cj0KCQjwj7v0BRDOARIsAGh37iosUEiVTPVg1DeiSQh8kKXzqf9s8ANODWEPLUMVjp6WVCYX7jTNq90aAu2qEALw_wcB)

あとは作るものを決めて、ひたすらググって物作りをしました。書籍の一部を会社経費で購入していただいた事もあり、この1年でFlutterのキャッチアップにかけた費用は5千円程度です。

ネットに有益な情報が沢山転がっているので、それをつなぎ合わせたり、とにかく手を動かしてプログラム資産を増やしていきました。

#3. 設計

本アプリの全体設計は以下の通りです。

![名称未設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/0e433347-a661-6073-2506-64fe47caf540.png)

アプリにはチャット機能などは持たせず、作成したデータをSNSへシェアすることを前提とした作りにしています。

データベースはSQLiteを利用し、バックアップを行う際にFirebaseのAuthとStorageを使っています。

Flutterの状態管理はprovider + ChangeNotifier。Rxは極所的に使っています。FlutterはRxに依存しなくてもアプリが作れるようになっています。

私がFlutterを学習し始めた時はGoogle I/OでBLoC (Business Logic Component) パターンを使う事が推奨されていましたが、途中からprovider推奨に変わりました。

BLoCよりもproviderを使った方が良い理由については以下の記事がわかりやすいと思います。
[まだ BLoC で消耗してるの？](https://ja.unflf.com/tech/flutter/provider/)

#4. 開発期間

本アプリは開発着手からリリースまでに8ヶ月程かかっています。会社勤めをしながらの個人開発なのでフル稼動ではないですが、ほぼ毎日開発しました。(8ヶ月の間で5日位しか休んでないですが、開発者自身は刺激的な毎日を楽しんできました。落ち着いたらゆっくり動物の森をやります)

FlutterはWidgetによりレイアウトを構築しますが、Widgetを組み立てる感覚はゲームのテトリスと似ており、楽しく開発しやすいです。

また、Widgetによるレイアウト構築はレイアウト間の縛りが非常に少ないのでレイアウトの再構築もしやすいです。(一言で言うと仕様変更に強いです)

楽しく開発しやすいFlutterだからこそ没頭できたし、iOS・Android両対応アプリを作れたのは大きな収穫です。

会社の業務でもFlutterでアプリを開発しており、業務と個人開発の相互作用を生み出せていると実感しています。

#5. アプリの規模感

本アプリのlib直下でclocを実行した結果は以下の通りです。Flutterでアプリ開発をしたことがある人であればわかると思いますが、それなりの規模感です。

<img width="565" alt="スクリーンショット 2020-04-25 8.03.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/ff237f89-b627-4c83-7993-d919b4219471.png">


clocは[Homebrew](https://brew.sh/index_ja)をインストールした後に、以下のコマンドを叩くとインストールできます。

```
brew install cloc
```

インストールした後に以下のコマンドを叩くと上記のようにファイルやコメントの数を出力できます。

```
cloc /Users/xxx/xxx/xxx/xxx/lib(lib直下までのパス)
```

#6. 初回起動時の画面説明

アプリの初回起動時にはアプリの導入画面を表示しています。導入画面にはアプリの概要とアプリを使うメリットを表示しています。

<img width="438" alt="スクリーンショット 2020-04-22 8.59.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/ae58492d-bd12-9f14-5745-d2f0905865b4.png">

導入画面を表示する際は以下のパッケージを使っています。
[introduction パッケージ](https://pub.dev/packages/introduction_screen)

```
pages: [
  PageViewModel(
    title: 'タイトル',
    body: '本文',
    image: const Center(
      child: Icon(
        Icons.fitness_center,
        size: 200,
        color: Colors.red,
      ),
    ),
    ...省略
```
introductionパッケージのpages(配列)にページごとのデータ(タイトル・本文・画像)を設定するだけで導入画面のUIが素早く作れます。(Flutterだとパッケージを使わなくてもこのようなUIの作り込みはしやすいです)

最後のページで確定ボタンが押されたらonDoneメソッドが呼ばれます。

```
onDone: () {
    ...保存処理などは省略
}
```

onDoneのタイミングでSharedPreferencesの値(導入画面表示判定フラグ)を更新し、一度だけ導入画面を表示しています。

SharedPreferencesを扱う際は以下のパッケージを使っています。
[shared_preferences パッケージ](https://pub.dev/packages/shared_preferences)

ネイティブでローカルにデータを保存する場合、iOSはUserDefaults。AndroidだとSharedPreferencesを使います。

この保存処理をFlutterで行う場合、shared_preferencesパッケージを使うことで1コード済みます。(shared_preferencesパッケージに限らず、FlutterだとiOS・Android個別にプログラムを書く必要がほとんどないです)

ローカルにデータを保存する時に、DBへデータを保存する必要がない情報はSharedPreferencesに保存します。

DBよりもSharedPreferencesを使った方が簡単だから何でもSharedPreferencesで良いと考えて作ると、コードを書いた人や他の開発者が後で苦しむことになります。

SharedPreferencesは、値を保持し続ける必要性が低いケース(設定画面項目や起動判定など)で使いましょう。

<img width="438" alt="スクリーンショット 2020-04-22 8.59.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/910cb3b7-aea1-8ff5-14f3-b6a58fe1090f.png">

部品ごと(Widgetごと)に説明を入れる際は、以下のパッケージを使っています。
[tutorial_coach_mark パッケージ](https://pub.dev/packages/tutorial_coach_mark)

はじめにTargetFocusの配列(targets)と[GlobalKey](https://api.flutter.dev/flutter/widgets/GlobalKey-class.html)(lockTutorialKey)を宣言します。

```
// TargetFocusとGlobalKeyの宣言
List<TargetFocus> targets = [];
GlobalKey lockTutorialKey = GlobalKey();
```

その後、targetsに表示したい情報(表示するコンテンツやGlobalKey)をadd(追加)します。

```
// 表示したい内容を設定
targets.add(
  TargetFocus(
    identify: 'Target 1',
    keyTarget: lockTutorialKey,
    contents: [
      ContentTarget(
        align: AlignContent.bottom,
        child: Text('説明文を設定'),
      )
    ],
    shape: ShapeLightFocus.RRect,
  ),
);
```


次にページ側のWidget(画面表示側の部品)のkeyにもkeyTargetでも指定したGlobalKeyを設定します。


```
CircleAvatar(
  key: lockTutorialKey,
  radius: 22,
  child: Icon(
    Icons.lock,
    size: 24,
  ),
)
```

最後にTutorialCoachMarkのshowメソッドを呼ぶと部品ごとに説明を表示できます。


```
TutorialCoachMark(
  ...省略
).show();
```

アプリ導入時の説明をどこまでやるかはしっかりと考えて実装しないと開発者の自己満足になります。アプリの内容がシンプルであれば入れる必要はないです。無駄に説明を長くすると、面倒だと感じたユーザーはアプリを使わずして離脱します。

本アプリは仕様が特殊なため、適度に説明を入れて仕様を把握しやすくしています。(説明なしで直感的にわかるUIを構築するのが大前提で、それをやり尽くしても内容が把握しにくい部分に説明を入れるのが良いと思います)


#7. 入力画面

**BottomNavigationBar**
画面下部のナビゲーションでページを切り替えたい場合、[BottomNavigationBar](https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html)を使います。

本アプリでは、入力画面を最初に表示して、アプリ起動後にいきなりデータを入力できるようにしています。

<img width="438" alt="スクリーンショット 2020-04-22 9.04.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/be361e70-4ccf-2b42-025d-225bb6a34381.png">

**OutlineButton**

ボタンの周りに線をつけたい場合は[OutlineButton](https://api.flutter.dev/flutter/material/OutlineButton-class.html)を使います。

```
OutlineButton(
  highlightedBorderColor: Colors.grey,
  onPressed:(){},
  child: Text('テキスト'),
  borderSide: const BorderSide(color: Colors.grey),
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(10),
  ),
)
```

borderRadiusは角の丸みを設定しています。

**IconButton**

アイコンだけのボタンを使いたい場合は[IconButton](https://api.flutter.dev/flutter/material/IconButton-class.html)を使います。

```
IconButton(
  icon: const Icon(Icons.chevron_right),
  onPressed: (){},
)
```

ここで指定している**Icons.chevron_right**はFlutterが用意している[Icons class](https://api.flutter.dev/flutter/material/Icons-class.html)です。本アプリ内部のアイコンは全てIcons classを使っています。

ネイティブの時は自分で画像を作っていたので、その工数がなくなった事は非常に大きいです。(逆に言うと、アプリ開発のハードルが下がる事で、アプリの数が増え、アプリの競争率は上がると思います)

**ListTile**

リスト項目を表示したい場合は[ListTile](https://api.flutter.dev/flutter/material/ListTile-class.html)を使います。

```
ListTile(
	leading: const Icon(Icons.flight_land)
	title: Text('タイトル'),
	subtitle: Text('サブタイトル'),
	trailing: const Icon(Icons.more_vert),
)
```

ListTileにはリスト項目を形成するためのテンプレートが豊富に用意されています。ListTileを使う必要がない行データやListTileでは実現が難しい行データを作りたい場合、[Row](https://api.flutter.dev/flutter/widgets/Row-class.html)を使います。

**Row**

```
Row(
  children: <Widget>[
    Text('1'),
    Text('2'),
  ],
)
```
#8. 入力ダイアログ

<img width="438" alt="スクリーンショット 2020-04-22 8.57.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/be6829da-d15b-8e0f-a3bc-26d8d99f4919.png">


**AlertDialog**

本アプリでは[AlertDialog](https://api.flutter.dev/flutter/material/AlertDialog-class.html)の上にボタンやテキストを表示しています。

```
AlertDialog(
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(8),
    ),
    title: Text(l10n.weight),
    content: Row(
      children: <Widget>[
        // テキストフィールド
      ],
    ),
    actions: <Widget>[
      // 各種ボタン
    ],
  )
```

**TextField**

[TextField](https://api.flutter.dev/flutter/material/AlertDialog-class.html)を使ってテキストを入力できるようにしています。ダイアログの表示と共にテキストにフォーカスを当てたいので、autofocusにtrueを設定しています。

```
TextField(
	autofocus: true,
	inputFormatters: [WeightFormatter()],
...省略
```

また、本アプリでは[TextInputFormatter](https://api.flutter.dev/flutter/services/TextInputFormatter-class.html)を使っています。


アプリを使ってデータを入力するとわかると思いますが、体重や体脂肪率を入力する際にユーザーが小数点を入力しなくてもアプリが小数点が打ち込んだり、小数点を2つ打ち込めないような制御が入っています。

これはTextInputFormatterを使い、特定の値が入力された時に入力値をアプリ側で制御・変更しているからです。開発者が入力操作を工夫する事でユーザーは少ない手数でデータを登録できるようになります。

以下のようにTextInputFormatterを継承したクラスを作成すれば入力された値を制御したり、書き換える事ができます。

```
class WeightFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    final newText = newValue.text;
    final newTextLength = newText.length;

    if (newText == '0') {
      return oldValue;
    }

    return newValue;
  }
}

```

作成したクラスをinputFormattersの引数に指定するだけです。複雑な制御処理をTextInputFormatterを継承したクラスで実装すれば良いので、メインコードの肥大化を抑えられます。

**RaisedButton**

```
RaisedButton(
  onPressed: () {},
  child: const Text('ボタン名'),
),
```

ダイアログ上に[RaisedButton](https://api.flutter.dev/flutter/material/RaisedButton-class.html)(入力補助ボタン・進むボタン・確定ボタン)を表示しています。

本アプリでは前回入力された体重や体脂肪率の値をSharedPreferencesに保存し、次にダイアログが表示された時に入力補助ボタンとして表示しています。入力補助ボタンには小数点以前の値を表示し、ボタンがタップされた時にテキストに入力値を反映させています。

例えば「72.8kg」を入力したい場合、入力補助ボタン(「72.」と表示されているボタン)のタップ後に「8」を入力すれば「72.8」を入力できます。ユーザーが楽にアプリを使えるようにする事で他のアプリとの差別化を図っています。

#9. カレンダー管理

標準的なカレンダーはこちら

[CupertinoDatePicker](https://api.flutter.dev/flutter/material/RaisedButton-class.html)
![cupertino-date-picker.png]
(https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/e782c8b5-3bb8-3645-0f24-ddd982f79602.png)

[DatePicker](https://api.flutter.dev/flutter/material/showDatePicker.html)
![components_pickers.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/eff491c4-eee0-e7e7-ba0a-8f392b75a68e.png)

本アプリではデータが登録されている日付に灰色の丸をつけたり、シンプルなUIを作りたかったので、以下のパッケージを使っています。
[flutter_calendar_carouselパッケージ](https://pub.dev/packages/flutter_calendar_carousel)

<img width="438" alt="スクリーンショット 2020-04-22 8.57.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/2ea36263-cd9c-d4fc-8cf1-c2d441ad3ac0.png">

このパッケージのカレンダーをダイアログとして表示するために[showDialog](https://api.flutter.dev/flutter/material/showDialog.html)を使っています。


```
await showDialog<void>(
  context: context,
  builder: (BuildContext context) {
    final mediaQuery = context.mediaQuery;
    return MediaQuery(
      data: mediaQuery.copyWith(
        viewInsets: mediaQuery.viewInsets -
            const EdgeInsets.symmetric(horizontal: 16),
      ),
      child: CalenderDialog(
        selectedDateTime: date,
        dateList: dateList,
        onDayPressed: (date) {
          ...省略
        },
      ),
    );
  },
);
```

カレンダー表示部分はメインの処理と分けるためにCalenderDialogというクラスを作っています。CalenderDialogクラスの中では[Dialog](https://api.flutter.dev/flutter/material/Dialog-class.html)を使っています。

このDialogは内部で余白(horizontalのpadding: 40.0)が指定されているため、[SizedBox](https://api.flutter.dev/flutter/widgets/SizedBox-class.html)でサイズを指定してもレイアウトが変わりません。そのため、[MediaQuery](https://api.flutter.dev/flutter/widgets/MediaQuery-class.html)のサイズを上書きして、Dialogのサイズを変更しています。

#10. 写真撮影とカメラライブラリ選択

[showModalBottomSheet](https://api.flutter.dev/flutter/material/showModalBottomSheet.html)でカメラ撮影かアルバム選択を選べるようにしています。

<img width="438" alt="スクリーンショット 2020-04-22 8.57.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/73f4a021-c38f-380b-5d39-a47966c9d27f.png">

ボタンが押された後に使っているパッケージは5つ。

[permission_handlerパッケージ](https://pub.dev/packages/permission_handler)
カメラや写真ライブラリへのアクセス許可を表示するためのパッケージ

[image_pickerパッケージ](https://pub.dev/packages/image_picker)
カメラや写真ライブラリを表示するためのパッケージ

[image_cropperパッケージ](https://pub.dev/packages/image_cropper)
写真を撮影した後の画像をトリミングするためのパッケージ

[flutter_image_compressパッケージ](https://pub.dev/packages/flutter_image_compress)
画像を圧縮するためのパッケージ

[image_gallery_saverパッケージ](https://pub.dev/packages/image_gallery_saver)
画像を端末に保存するためのパッケージ

**permission_handler**を使う際はiOSのpodファイルで不要なパーミッションリクエストを削除しないとアプリ申請時に弾かれます。以下の記事を参考にするとその問題を回避できます。
[不要なパーミッションリクエストを削除する](https://qiita.com/quqjp/items/3a0ccb45aa5c04053b93)

**image_gallery_saver**は写真撮影時のみ端末へ画像を保存しています。(アルバムの中から画像を選んだ時に画像を保存すると、同じ画像が端末に保存されるため、撮影を行った時のみ画像を保存するようにしています)

撮影した画像を端末に保存したくない人もいると思いますので、設定画面で「写真を端末に保存」のオン・オフを切り替えられるようにしています。

<img width="438" alt="スクリーンショット 2020-04-22 9.17.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/53934946-0987-8345-7475-146102eea480.png">

#11. データ登録完了時

データの登録を完了した時に見た目で達成感を味わってもらうために以下のパッケージを使っています。
[awesome_dialog パッケージ](https://pub.dev/packages/awesome_dialog)

<img width="438" alt="スクリーンショット 2020-04-22 9.23.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/12aeda94-d7dc-6096-aa47-9a3adc2003e9.png">

アニメーションの動きが非常に良く、確認ダイアログやエラー発生時もこのダイアログを使ってデザインを統一させています。

あと、データが登録できたことを体感してもらいやすくするために以下のパッケージを使っています。このパッケージにより振動(フィードバック)を発生できます。
[vibrate パッケージ](https://pub.dev/packages/vibrate)


```
Vibrate.feedback(FeedbackType.medium);
```

指定する引数によって、振動の大きさを変えられます。本アプリではデータの登録が完了したり、データの削除が完了した時は引数に「FeedbackType.medium」を設定し、設定画面項目などのスイッチ切り替えを行なった場合は「FeedbackType.light」を設定しています。

振動の強弱によって、操作の違いを表現しています。

**ダイアログを自動で閉じる**

ダイアログを表示すれば操作が完了したことが視覚的にわかりますが、ダイアログが表示されたままだとユーザーは画面をタップしてダイアログを閉じる必要がでてきます。その手間を省くために遅延実行でダイアログを閉じるようにしています。

```
Future.delayed(
  Duration(milliseconds: 1800),
  () {
    Navigator.of(context).pop(true);
  },
);
```
上記の処理によりダイアログを自動で閉じる事ができます。しかし、自動でダイアログが閉じられる前に画面タップによる閉じるが行われたら2回のpop(戻る処理)が走ってしまうため、実際にアプリを作る際はそれを考慮した実装が必要になります。(onDissmissCallbackを考慮した実装が必要になります)

#12. ビフォーアフター比較画面

<img width="438" alt="スクリーンショット 2020-04-22 8.56.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/487c450c-f71a-77d5-9189-7b7f38776ef4.png">

**DefaultTabController**
タブを使って画面切り替えを行いたい場合、[DefaultTabController](https://api.flutter.dev/flutter/material/DefaultTabController-class.html)を使います。

```
DefaultTabController(
  length: 2,
  child: Scaffold(
    appBar: ...省略,
    body: TabBarView(
      children: ...省略
    ),
  ),
);
```

**Column**
縦にデータを並べて表示したい場合、[Column](https://api.flutter.dev/flutter/widgets/Column-class.html)を使います。

```
Column(
  children: <Widget>[
    Text('1'),
    Text('2'),
    ),
  ],
)
```

**FittedBox**


特定の領域の高さや横幅に合わせてサイズを調整したい場合、[SingleChildScrollView](https://docs-flutter-io.firebaseapp.com/flutter/widgets/FittedBox-class.html)を使います。

```
FittedBox(
  child: ... 省略,
)
```

アプリは自分が持っている端末で確認した時はレイアウトが崩れず、小さい端末を使ってるユーザーがアプリを使った時にレイアウトが崩れる時があります。ストアに公開するアプリを開発する時は異なる端末サイズでもレイアウトが崩れないようこの辺りを考慮しながら作る必要があります。

**Card**
カード形式でUI表示を行いたい場合は[Card](https://api.flutter.dev/flutter/material/Card-class.html)を使います。

```
Card(
  child: ... 省略,
)
```

**CircleAvatar**
円形でUI表示を行いたい場合は[CircleAvatar](https://api.flutter.dev/flutter/material/CircleAvatar-class.html)を使います。


```
GestureDetector(
  child: CircleAvatar(
    radius: 22,
    child: Icon(
      Icons.lock,
    ),
  ),
  onTap: (){},
)
```

このサンプルではCircleAvatarを[GestureDetector](https://api.flutter.dev/flutter/widgets/GestureDetector-class.html)で囲っています。CircleAvatarは円形の見た目を作るウィジェットです。タップした時に何か処理を行いたい場合、GestureDetectorをつければイベントの処理ができます。

**FloatingActionButton**
ユーザーにボタンタップアクションを促す時に使います。

```
floatingActionButton: FloatingActionButton(
  child: const Icon(
    Icons.check,
  ),
  onPressed: (){},
),
```

#13. ビフォーアフターのロック

<img width="438" alt="スクリーンショット 2020-04-23 14.55.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/ca210920-aa9b-59b8-f0aa-a207a0b6a374.png">

入力画面からデータを登録した時は日付の大小比較を行い、beforeやafterにデータをセットしています。

でもbeforeのデータを固定のままにしたい人やbeforeとafterの両方を固定しておきたい人もいると思います。本アプリではそれを可能にするためにロック(固定化)機能を持たせています。

そして、このロックマークがタップされた時はSnackBarを表示しています。何か操作を確定したり、操作の確認を挟みたい場合はダイアログが良いと思いますが、そこまで目立たせる必要がなく、何が行われたのかをユーザーへ知らせたい時にSnackBarは有効だと思います。

私の場合、以下のカスタムクラスを作成し、それを呼び出しています。

```
class CustomSnackBar {
  CustomSnackBar({
    @required this.context,
    @required this.message,
  });

  final BuildContext context;
  final String message;

  void show() {
    Scaffold.of(context).removeCurrentSnackBar();

    final snackBar = SnackBar(
      content: Row(
        children: <Widget>[
          Expanded(child: Text(message)),
          GestureDetector(
            child: Icon(Icons.expand_more),
            onTap: Scaffold.of(context).removeCurrentSnackBar,
          ),
        ],
      ),
      duration: const Duration(seconds: 3),
    );

    Scaffold.of(context).showSnackBar(snackBar);
  }
}
```

SnackBarが表示されてから3秒後にSnackBarを閉じるようにしています。

SnackBarの中には「SnackBarAction」というプロパティが用意されており、SnackBarActionの中にはLabelというプロパティが存在します。このLabelを使うことで「閉じる」というテキストを表示させることはできます。

ですが、可能な限り直感的に操作できるアプリに仕上げたかった(ユーザーにテキストを読む煩わしさを与えたくなかった)ので、contentの中にテキストとアイコンを設定し、下矢印アイコンを表示させてSnackBarを閉じれるようにしています。

#14. ビフォーアフターの履歴追加時

<img width="438" alt="スクリーンショット 2020-04-22 8.56.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/7a960ecc-37de-1204-7cd7-7023b9ea081d.png">

ビフォーアフターの履歴追加が完了した時はダイアログに「履歴を見る」ボタンを表示しています。

「履歴を見る」ボタンを表示する事でスワイプしたり画面上部のタブ切り替えエリアをタップしなくても画面の切り替えをできるようにしています。

プログラム側でタブ移動を行いたい場合、タブのインデックスを変更します。

```
DefaultTabController.of(context).index = 1;
```

#15. ビフォーアフターの履歴一覧画面

<img width="438" alt="スクリーンショット 2020-04-22 8.56.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/7f213c4e-d392-b3ac-c17a-6bde74a1d5cd.png">

履歴への追加を行うと、before・afterの差分を見ることができます。アプリにデータを記録する価値を持たせることでユーザーはアプリを気に入ってくれます。記録する価値を提供し、ユーザーにデータを登録してもらえればアプリの離脱を防げます。

アプリの離脱を防げれば、徐々にアクティブユーザーが増え、アクティブユーザーが増えると、アプリのランキングが上がりやすくなります。

履歴の一覧表示には[AnimatedList](https://api.flutter.dev/flutter/widgets/AnimatedList-class.html)を使っています。AnimatedListを使うとデータの追加や削除操作を行う際にアニメーションをつけることができます。

#16. ビフォーアフターの履歴のシェア

<img width="438" alt="スクリーンショット 2020-04-22 8.56.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/2d93d092-5ced-b5b5-b81e-53b2a2ad0871.png">

履歴のセルでシェアボタンを押した時に以下のパッケージを使っています。このパッケージにより左右にスワイプしたり左右の矢印ボタンをタップした時にページの切り替えができます。
[flutter_swiper パッケージ](https://pub.dev/packages/flutter_swiper)

投稿するSNSやユーザーの要望により、最適なシェア方法は異なります。例えば、Twitterでbefore・afterの画像を2枚貼り付けたい場合、縦2枚が最適です。選択肢を用意する事でユーザーは不満なくアプリを使えるようになります。

最適なシェア方法を選んだ後は画像をシェアするために以下のパッケージを使います。
[share_extendパッケージ](https://pub.dev/packages/share_extend)

```
await ShareExtend.share(
  file.path,
  'image',
);
```

ただし、このパッケージは画像とテキストをまとめて添付できません(2020年4月22日時点)

画像とテキストをまとめて添付する際は以下のパッケージを使っています。
[esys_flutter_shareパッケージ](https://pub.dev/packages/esys_flutter_share)

```
await Share.files(
  'タイトル',
  {
    'aaa.png': Uint8List型の画像データ,
  },
  'image/png',
  text: '出力するメッセージ',
);
```

esys_flutter_shareは画像やテキストの単体添付も可能ですが、share_extendの方が実装がシンプルなので場合分けして使っています。(いつかshare_extendでもesys_flutter_shareと同等の機能が使えるようになることを予想して、基本的にはスマートに使えるshare_extendを使っています)

本アプリでは、画像をシェアした時にbefore・afterのラベルを画像の上に付加しています。現在、SNSでビフォーアフターの画像をシェアしている方達は画像の上に手動でラベルを重ねていると思いますが、このアプリを使う事でその操作は不要になります。

ちなみにこれはFlutterの[RenderRepaintBoundary](https://api.flutter.dev/flutter/rendering/RenderRepaintBoundary/toImage.html)を使う事で実現しています。RenderRepaintBoundaryを使うとウィジェットを画像として切り取れます。

#17. 写真管理画面

写真管理機能を持たせて、見た目の変化だけを振り返れるようにしています。

<img width="438" alt="スクリーンショット 2020-04-22 8.56.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/c4b28af6-a14d-ff3e-d76e-c6c11a6b2935.png">

年別の写真をタップした時に月別の写真を表示し、月別の写真をタップしたら全ての写真を表示しています。(画面切り替えを行っています)

画面を切り替えた時はタップした写真に紐づく情報が見えるようにスクロール処理を走らせていますが、それを行うために以下のパッケージ内のScrollablePositionedListを使っています。
[flutter_widgets パッケージ](https://pub.dev/packages/flutter_widgets)

特定の位置までスクロールさせる話は以下の記事がわかりやすいです。
[特定の位置までのスクロールを実現する3つのパッケージ](https://note.com/kitoko552/n/n52cb06e79d49)

#18. 写真拡大表示

<img width="438" alt="スクリーンショット 2020-04-22 11.43.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/95b1d57b-0595-e604-56f4-6378f35420fb.png">

全てのタブの中に表示されている写真をタップした時は画像を拡大表示しています。拡大画像を上や下に引っ張った時に一覧画面に戻れるように以下のパッケージを使っています。
[full_screen_imageパッケージ](https://pub.dev/packages/full_screen_image)

画像の拡大表示をする際はアニメーションウィジェット[Hero](https://flutter.dev/docs/development/ui/animations/hero-animations)を使っています。

また、横スワイプにより写真の前後切り替えができるように以下のパッケージを使っています。
[photo_viewパッケージ](https://pub.dev/packages/photo_view)

#19. グラフ表示画面
<img width="438" alt="スクリーンショット 2020-04-22 8.56.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/4f930779-f91c-d3b2-2999-d4811d245f60.png">

グラフ表示を行う際は以下のパッケージを使っています。
[fl_animated_linechart パッケージ](https://pub.dev/packages/fl_animated_linechart)

ただし、このパッケージ任せだとこのようなUIの作り込みはできないので、パッケージの一部を改造しています。

グラフ表示対象データの切り替え部分は[CupertinoSlidingSegmentedControl](https://api.flutter.dev/flutter/cupertino/CupertinoSlidingSegmentedControl-class.html)を使っています。

```
CupertinoSlidingSegmentedControl(
  groupValue: 0,
  children: {
    0: Text('1つ目のタイトル'),
    1: Text('2つ目のタイトル'),
    2: Text('3つ目のタイトル'),
    ...省略
  },
  onValueChanged: (int index) {

  },
)
```

#20. 設定画面

<img width="438" alt="スクリーンショット 2020-04-22 8.56.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/4d8dee7e-08cd-22ae-c31c-c6c7a0fd253d.png">


**お問い合わせ**

お問い合わせメール送信のために以下のパッケージを使っています。
[flutter_email_sender](https://pub.dev/packages/flutter_email_sender)

```

final email = Email(
        body: '本文',
        subject: '題名',
        recipients: ['メールアドレス'],
      );

// メーラー起動
FlutterEmailSender.send(email);
```

**レビューを書く**

ストアの画面に遷移しアプリのレビューを書いてもらうために以下のパッケージを使っています。
[app_review](https://pub.dev/packages/app_review)

```
// ストアのレビュー画面に遷移
AppReview.writeReview;
```

**バージョン番号**

バージョン番号を表示するために以下のパッケージを使っています。
[get_version](https://pub.dev/packages/get_version)

```
// バージョン番号取得
final projectVersion = await GetVersion.projectVersion;
```

#21. バックアップ機能

<img width="438" alt="スクリーンショット 2020-04-22 8.56.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/a6ed00ae-ee83-9ca4-fbf1-ddf68886a3f8.png">

ボタンが押された後に使っているパッケージは5つ。

[firebase_authパッケージ](https://pub.dev/packages/firebase_auth)
FirebaseのUser認証を行う

[google_sign_inパッケージ](https://pub.dev/packages/google_sign_in)
Googleサインインを行う

[apple_sign_inパッケージ](https://pub.dev/packages/apple_sign_in)
Appleサインインを行う

[firebase_storageパッケージ](https://pub.dev/packages/firebase_storage)
FirebaseのStorageを操作する

ハマりポイントはこちら

```
final FirebaseApp app = await FirebaseApp.configure(
    name: 'test',
    options: FirebaseOptions(
      googleAppID: (Platform.isIOS || Platform.isMacOS)
          ? '1:159623150305:ios:4a213ef3dbd8997b'
          : '1:159623150305:android:ef48439a0cc0263d',
      gcmSenderID: '159623150305',
      apiKey: 'AIzaSyChk3KEG7QYrs4kQPLP1tjJNxBTbfCAdgg',
      projectID: 'flutter-firebase-plugins',
    ),
  );
```
Firease Storage のサンプルには上記のコードが書かれていて、その通りに書いたのですが動きませんでした。
上記のような処理を書く必要はなく、FirebaseStorageのインスタンスを生成し、それを操作するだけで大丈夫でした。

```
final FirebaseStorage _storage = FirebaseStorage.instance;
```

このハマりポイントについての詳細は以下の記事がわかりやすいです。
[Flutter で FireaseApp.configure はマルチプロジェクトの時だけ呼ぶ](https://blog.takuchalle.dev/post/2019/03/13/not_to_use_firebase_configure/)


#22. パスロック機能

<img width="438" alt="スクリーンショット 2020-04-23 10.57.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285476/932e296c-2843-dae8-7d29-7a6d1a62b61a.png">

本アプリではデータをプライベートにできるようにパスロック機能をつけています。

パスロックのFaceIDロック解除や指紋認証解除行うために、以下のパッケージを使っています。
[local_auth](https://pub.dev/packages/local_auth)

local_authプラグインの使い方は以下の記事がわかりやすいです。
[Flutterで顔認証/指紋認証を簡単に実装する](https://qiita.com/coka__01/items/76af4ea73a6a8c8fa135)

このプラグインのハマりポイントはこちら(2019年9月頃からハマっている人がチラホラ)
https://github.com/flutter/flutter/issues/41119

私の場合、以下の対応で解決しました。
https://twitter.com/cloverkizuna/status/1245857322963832833

パスロック画面を表示する際はFlutter側でライフサイクル監視を行います。そして、バックグラウンド状態になった時にパスロックを表示させます。

ただし、バックグラウンドになるタイミングでパスロック画面を呼び出したとしてもすぐにロック画面が表示されません。(画面遷移時のアニメーションをなくしても結果は変わりません)

次にアプリがフォアグラウンド状態になったタイミングで遅れてロック画面が表示されます。これはパスロックとして完全な状態ではありません(一瞬パスロックをかける前の画面が表示されるのは望ましくありません)

私の場合、ネイティブ側で以下の処理を書くことでそれを回避しました。

iOS

```
    override func applicationDidEnterBackground(_ application: UIApplication) {
        if(isPasswordLock()){
            self.window.resignKey();
            self.window.isHidden = true;
        }
    }
    
    override func applicationWillEnterForeground(_ application: UIApplication) {
        if(isPasswordLock()){
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
                self.window.makeKeyAndVisible();
               self.window.isHidden = false;
            }
        }
    }
```

Android

```
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)

    if(getIsPasswordLock()){
      flutterView.setBackgroundColor(Color.BLACK)
    } else {
      flutterView.setBackgroundColor(Color.TRANSPARENT)
    }
  }

  override fun onStart() {
    super.onStart()

    if(getIsPasswordLock()){
      Handler().postDelayed({
        flutterView.setBackgroundColor(Color.TRANSPARENT)
      }, 800)
    }
  }

  override fun onPause() {
    super.onPause()

    if(getIsPasswordLock()){
      flutterView.setBackgroundColor(Color.BLACK)
    }
  }
```

iOSアプリのハマりポイント

上記のコードの中でiOSアプリでは以下の処理を入れています。

```
self.window.resignKey();
と
self.window.makeKeyAndVisible();
```

これを入れなかった場合はパスロックを解除してアプリを起動した後にどこかの画面でキーボードを表示しようしたタイミングでアプリがクラッシュします。

#23. このアプリを作った理由

私自身、健康管理をアプリでしたいと思い、色んなアプリを使ったものの全て続きませんでした。

体重や体脂肪率などを入力した後にグラフを見る。これが今ストアに上がっているアプリの標準的な動きだと思います。

でもグラフを見てもトレーニングをした成果をあまり実感できずに終わりました。グラフの推移は参考にはなるものの、それを見る行為にそこまで面白みを感じませんでした。

一度、自分で身体の状態を撮影し、その後トレーニングを続けてどのような変化があったのかを見比べた時にトレーニングの成果を感じられ、これは面白いと思いました。

しかし、ストアに上がっているアプリを見ても、自分が求める機能を持ち合わせているものが見当たらず、自分でアプリを作ろうと思いました。

#24. インフルエンサーマーケティング

このアプリはインフルエンサーマーケティングを考えて作っています。

インフルエンサーには沢山のファンがいます。インフルエンサーの人がとる行動を見て、そこから刺激や学びを得て、真似る人が沢山います。

ではどのような人がインフルエンサーになっていますでしょうか？

私の分析では「行動力がある人」です。

インフルエンサーの中には、筋トレをしてシェイプアップをしてモデルになったり、アルバイトとして働いていた人が筋トレをして見た目が変わって影響力を持ち、経営者になっている方達がいます。

そしてこの層のインフルエンサーは既に筋トレビフォーアフターの発信をしています。

そういう行動力のある人の目にとまる可能性があると考えてアプリを作っています。世界的に見ても運動習慣がない人よりも運動習慣がある人の方が経済的にも恵まれています。

身体を動かしたり、鍛えている人たちは行動力も影響力もあります。

このアプリを通じて運動の必要性を感じ、元気になる人が増えていけば、未来はより明るくなると考えています。

#25. マネタイズ

このアプリにはまだ広告を入れておらず、機能制限もかけていません。

なので、現時点では1円も収益が入らない状態です。でも今はこれで良いと考えて、あえてそうしています。

ちなみにマネタイズをする時期は1年先でも良いと思っています。これは個人開発だからできる考えですね。

コロナの影響もあって、今はアプリの売上を考えるよりもみんなで元気に過ごす事を考える方が大切です。筋トレは自宅でもできますし、運動を意識して身体を健全な状態に保つ事が大切です。

ちなみに私が開発したアプリで最も人気のある文字数カウントメモも今年の4月に広告表示を減らし、心理的な負担を感じずに日々を過ごしてもらえるようにしました。

https://twitter.com/cloverkizuna/status/1245201057266851841?s=20

そのスタンスがいつか評価される時がくれば嬉しいです。

個人で費やした時間やデザイン費用は自己負担になりますが、このアプリに必要性を感じる方が増えていけば自分がしてきた行動の信頼度合いが高まります。

信頼度が高まれば、そこには人が集まります。人が集まった時に次を考えようと思っています。

私の経験上、行動したり、自分が作ったアプリに共感してくれる人が増えていくと、その後の方向性は勝手に変わりますし、選択肢も広がります。なので、まずはアプリを作り、信頼を築き上げることが大切だと考えています。

あと、アプリを作りきればプログラム資産が増えます。プログラム資産を作れば同じような機能を作る必要性に迫られた時に何倍も早くアプリを作ることができます。

最初は大変ですが、予め作っておけば将来アプリ開発をする時のリスクヘッジもできます。

このアプリには広告を違和感なく入れられる箇所もあり、将来的に広告を入れる考えも当然あります。(将来的に広告 + サブスクが良いと思っています)

#26. まとめ

この記事ではアプリを作る際に使ったパッケージをオープンにしています。かなりの数のパッケージを落として確認してきたので、ここで紹介したパッケージは厳選された情報になっていると思います。

パッケージをうまく活用すれば開発が楽になりますし、開発したアプリを元に技術とテクニックを解説すればアプリの全体像を把握しやすいと思ったのでこのような内容にしました。

ユーザーにアプリを快適に使ってもらうための工夫やアプリを継続的に使ってもらうために何をしたのかも具体的に書きました。

勿論、ここには書ききれない程細かい仕様を考えてアプリを作っていますが、細かい仕様を書いてもそれはアプリ固有の情報であり、記事をご覧になられた方にとって参考にならない可能性もあると考えて情報をまとめました。

個人的にアプリ開発をする上では以下が大切だと思います。
・技術を学ぶ(これをしないとまず物作りができません)
・色んなアプリをダウンロードする
・自分で考えて物作りをする
・人の行動分析をする
・アプリで何かしらの問題を解決する
・様々なことにアンテナを張る

このアプリは沢山の人の情報がリンクして完成しました。Flutterをやり始める前には絡むことがなかった方ともネットを通じて一緒に盛り上がれたことも私にとって成功体験になりました。みなさん、本当にありがとうございました。