FlutterとFirebaseで開発した 英語の瞬間翻訳トレーニングアプリ Lala の技術



どうも、[shohei](https://twitter.com/hobbydevelop)です。

[株式会社Never](https://neverjp.com/)の代表です。

今回はFlutterとFirebaseで開発した英語の瞬間翻訳トレーニングアプリ Lala の技術を紹介します。

アプリはこちらからダウンロードできます（無料でご利用できます）。
[![icon_android_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/370e76b0-3670-c78b-8dd0-9d3606c8c515.png)](https://lala.support/)
・iOS
[https://apps.apple.com/jp/app/id1493691416](https://apps.apple.com/jp/app/%E8%8B%B1%E8%AA%9E%E3%81%AE%E7%9E%AC%E9%96%93%E7%BF%BB%E8%A8%B3%E3%83%88%E3%83%AC%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0-lala/id1493691416)

・Android
[https://play.google.com/store/apps/details?id=com.gmail.hukusuke1007.lala](https://play.google.com/store/apps/details?id=com.gmail.hukusuke1007.lala)

## 目次

- [概要](#概要)
- [開発期間](#開発期間)
- [設計](#設計)
- [主な機能](#主な機能)
- [UI/UXデザイン](#uiuxデザイン)
- [ユーザーレビュー](#ユーザーレビュー)
- [マーケティング](#マーケティング)
- [その他](#その他)
- [終わりに](#終わりに)

## 概要

まずはLalaの概要を紹介します。アプリストアの情報をそのまま掲載します。

```
Lalaは英語の会話力向上を目的とした瞬間翻訳トレーニングアプリです。
次々と出題される文章を瞬間的に翻訳（脳内翻訳）して会話力を鍛えるアプリです。

■ 使い方
無料でご利用頂けます。

チャット形式で出題される日本語を制限時間内に翻訳していきます。
出題された数秒後に英語の回答例が表示されます。

・対応言語（出題 => 回答例）
日本語 => 英語
英語 => 日本語

■ 特徴
・チャット形式で日本語とその回答例が出題されます
・日常会話で使われる英作文を用意しております
・オリジナルの英作文を作成して学習できます
・すぐに学習できるようUIを工夫しております
・アプリのデータは全てクラウド上で安全に管理しています
```

## 作ろうと思った経緯

英語の勉強をしていたのがきっかけです。

英語で会話できるよう勉強していた中で、Hapa英会話YouTubeで「瞬間翻訳トレーニング」という勉強法に出会いました。

[英語をパッと口にするコツ【#276】](https://www.youtube.com/watch?v=drWnC7BSHhM)

このYouTubeで紹介されている勉強法は、日本語で出題された文章を何も考えずに瞬時に英語に訳して伝えるトレーニングです。

実際やってみると分かると思うんですがこれがめちゃくちゃ難しいです。

そしてとても実践的な勉強法で効果があると思いこの手のアプリがないか探しました。

ところが自分が求めるようなアプリがなかったので、じゃあ自分で瞬間翻訳トレーニングのアプリを作ろうじゃないか思ったのが開発に至った経緯です。

## 開発期間

デザイン、設計、実装（アプリ、バックエンド）込みで、<b>約2人月（320時間）</b>です。
※アプリバージョン1.3.2までで

開発は私のみなので約2ヶ月分の稼働でリリースしました。

英作文の作成は他の方にお願いしています。

## 設計

全体設計は次のとおりです。

<img width="1000" alt="スクリーンショット 2020-03-01 17.24.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/d344bd4e-49ed-4ab8-11c3-4ba76129302e.png">

FlutterとFirebaseを採用した理由は実装コストの大幅な削減です。

内部設計としてはアプリ、バックエンド共にClean Architectureを採用しています。

Clean Architectureを採用した理由は責務をしっかり切り離せれる点とロジックの再利用性の高い設計ができるからです。

Clean Architectureの詳細については割愛しますが、こちらの文献を参考にして実装しています。
[AndroidやiOSのアプリ開発でいつも私がやっていることをFlutterでやってみた](https://speakerdeck.com/yasi/flutter-development-as-usual)


### アプリ

ディレクトリ構成は次のとおりです。

```sh
.
├── app.dart
├── app_bloc.dart
├── common
│   ├── extension
│   │   └── ....dart
│   ├── flavor.dart
│   ├── helper
│   │   └── ....dart
│   └── master
│       └── ....dart
├── di_container.dart
├── domain
│   ├── dto
│   │   └── ....dart
│   ├── model
│   │   └── ....dart
│   ├── repository
│   │   └── ....dart
│   ├── state
│   │   └── ....dart
│   ├── type
│   │   └── ....dart
│   └── use_case
│       └── ....dart
├── infrastructure
│   └── ....dart
├── l10n
│   └── ....dart
├── main.dart
├── main_development.dart
├── main_production.dart
├── main_staging.dart
└── presentation
    ├── common
    │   └── ....dart
    ├── pages
    │   └── ....dart
    ├── route
    │   └── ....dart
    └── widget
        └── ....dart
```

#### BLoCパターンで実装
アプリのpresentation層の設計はBLoCパターンを採用しています。

BLoCについてはこちらの記事を参考にしました。
[FlutterのBLoC(Business Logic Component)のライフサイクルを正確に管理して提供するbloc_providerパッケージの解説](https://medium.com/flutter-jp/bloc-provider-70e869b11b2f)

BLoCを採用した理由としては次のとおりです。

- 不要なリビルドを防いで動作を軽くするため
- Rx（ReactiveX）を使ってデータが流れてきた時に実行される実装をしたかったため
- 設計当初、Provider（ValueNotifierProviderなど）の存在を知らなかったため

BLoCで設計して実装してみたところ、Providerを使った設計をすれば良かったと思います。

理由はBLoCの実装コストが高いためです。

BLoCではイベントやデータの入出力を全てStream経由で行うためコード量がどうしても多くなりがちで実装に時間がかかってしまうからです（めんどくささを感じる）。

また自分は既にネイティブアプリでRxの知見を持っていたので問題なかったのですが、Rxを知らない状態からBLoCを採用するとRxの学習コストがかかってしまいます。

ただ、Rxを使うと「このデータが流れてきたらこの処理を実行する」といった実装ができるので、個人的に何をしたいのかわかりやすいという所感をもっています。

#### 今後はProviderでリファクタリングしていく予定

[provider](https://pub.dev/packages/provider)はValueNotifierProviderやChangeNotifier等を提供しているパッケージです。

providerが持つConsumerやSelectorなどの機能を使うことで対象のWidgetだけにデータを渡せることができるので不要なWidgetのリビルドを防ぐことができます（BLoCを使う目的を満たせれる）。

また、providerの実装コストがBLoCより低いと感じたからです。入出力のStreamを用意しなてもよくなり、dispose忘れのリスクがなくなります（BLoCに比べて実装に時間がかからない印象、かなり個人的な所感ですが）。

さらに、providerパッケージは開発が進んでおり、技術的に興味をもっているのでキャッチアップしていきたいというのも理由の一つです。~~本当はReactiveXに飽きた。~~

今は新しい機能は全てproviderで実装しています。BLoCとproviderがまざった設計になってしまっているので追々は全てproviderでリファクタリングをしようと考えています。

providerの使い方についてはこちらの記事を参考にしています。
[[Flutter] package:provider の各プロバイダの詳細](https://qiita.com/kabochapo/items/a90d8438243c27e2f6d9)

[2020/03/21追記]
[state_notifier](https://pub.dev/packages/state_notifier) でリファクタリングすることを決めました。

state_notifierについてはこちらの記事を参照してください。
[Flutter state_notifierいい感じなので使ったほうが良いですよ](https://qiita.com/_masaokb/items/fe77495db0aeba226d2a)

### バックエンド

バックエンドはFirebaseで構築しています。

| Firebase | 用途 |
|---|---|
|Firebase Authentication|ユーザー認証|
|Firestore|データベース|
|Cloud Storage|ファイルストレージ|
|Cloud Functions|APIサーバー|

Firebaseを使ったことある方はお馴染みの機能ですね。Firebase Authenticationとセキュリティルールを使っているのでアプリから直接データベースを呼ぶことができます。

#### Cloud Functionsの設計

アプリと同様にClean Architectureで設計しています。ディレクトリ構成は次のとおりです。

```sh
.
├── package-lock.json
├── package.json
├── src
│   ├── common
│   │   ├── helper
│   │   │   └── ....ts
│   │   └── master
│   │       └── ....ts
│   ├── domain
│   │   ├── dto
│   │   │   └── ....ts
│   │   ├── model
│   │   │   └── ....ts
│   │   ├── repository
│   │   │   └── ....ts
│   │   ├── type
│   │   │   └── ....ts
│   │   └── use_case
│   │       └── ....ts
│   ├── index.ts
│   ├── infrastructure
│   │   └── ....ts
│   ├── inversify.config.ts
│   ├── presentation
│   │   └── v1
│   │       ├── callable
│   │       │   └── ....ts
│   │       ├── firestore
│   │       │   └── ....ts
│   │       ├── index.ts
│   │       └── router
│   │           └── ....ts
│   └── test
│       ├── firebase_functions
│       │   ├── index.ts
│       │   ├── module
│       │   │   └── ...test.ts
│       │   └── use_case
│       │       └── ...test.ts
│       └── firestore_rule
│           ├── ...test.ts
│           └── index.ts
├── tsconfig.json
├── tslint.json
└── yarn.lock
```

APIのエンドポイントはこちらの記事を参考に設計しています。
[CloudFunctionsをきれいに整理したい。](https://qiita.com/1amageek/items/b93f8e4d99e3b6f423aa)

domain層のUseCaseおよびRepositoryへ実装部をDIしています。DIのライブラリは[inversify](https://github.com/inversify/InversifyJS)を使用しています。

テストは[Jest](https://jestjs.io/ja/)を使っています。Firestoreのセキュリティールールやdomain層以降のテストコードを作成して品質保証しています。

セキュリティルールのテストはこちらの記事を参考にしています。
[Cloud Firestoreのrulesのテストを全てローカルエミュレータを使うように書き換えた話](https://techlife.cookpad.com/entry/2018/11/05/143000)

## 主な機能

Lalaに実装されている主な機能を紹介します。

- ログイン機能
- 英作文のダウンロード
- オリジナル英作文の作成
- 学習結果の保存（スコア、メモ、学習回数など）
- アプリ内課金

### ログイン機能

Lalaは次のログイン機能を実装しています。

- Googleログイン
- Facebookログイン
- Appleログイン（iOSのみ）

ログインをGoogleとFacebookにした理由は、Mediumなど他のサービスでも使われているという点と幅広い世代で網羅できる点です。

過去に開発したアプリより、Twitter認証は幅広い世代を網羅できない点があったのと、電話番号認証はユーザーが利用するハードルが高いといった理由でやめました。

Appleログインを実装した理由はAppleのルールに従うためです（大人の事情）。
[【iOS】対応必須かも？Sign In with Appleまとめ（第一報)](https://qiita.com/shiz/items/5e094910f742c2ad72a4)

また、アプリをスムーズに使ってもらうため（離脱防止のため）にアプリ起動したらすぐに匿名認証でログインしています。英作文のダウンロード等をした際にアカウント認証でログインをして本アカウントへ切り替えれるようにしています。

<img width="375" alt="IMG_0621.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/ea192fd8-5f7e-1adf-5f3d-a9257bab66f1.png">

#### 実装ポイント

ログイン機能で利用するライブラリは次のとおりです。

- [flutter_facebook_login](https://pub.dev/packages/flutter_facebook_login)
- [google_sign_in](https://pub.dev/packages/google_sign_in)
- [apple_sign_in](https://pub.dev/packages/apple_sign_in)
- [firebase_auth](https://pub.dev/packages/firebase_auth)

facebook, google, appleログインできるプラグインを利用してログインに必要なトークンを取得します。取得後、FirebaseAuthと連携してログインします。

次のコードはGoogleログインの実装例です。

```dart
final _googleSignIn = GoogleSignIn(scopes: ['email',],); 
final _auth = FirebaseAuth.instance;

Future<void> loginWithGoogle() async {
  // Googleからトークン取得, これを呼ぶとアカウント情報入力画面が表示される
  final googleUser = await _googleSignIn.signIn();
  if (googleUser == null) {
    return null;
  }
  final auth = await googleUser.authentication;

  // ここからFirebase, Credentialを取得
  final authCredential = GoogleAuthProvider.getCredential(
      idToken: auth.idToken,
      accessToken: auth.accessToken
  );

  // ここからFirebaseAuthへ連携する, ログイン状態になる
  await _auth.signInWithCredential(signInResult.authCredential); 
}
```

また、アプリ起動時に匿名認証をしています。

```dart
Future<void> loginWithAnonymously() async {
   await _auth.signInAnonymously();
}
```

匿名認証することでアカウント認証しなくてもFirestoreのデータを取得できるようにするためです（このアプリを利用している場合のみFirestoreのデータを取得できる）。

```
アプリ起動 -> 匿名認証（自動） -> 英作文のダウンロード -> アカウント認証を求めてログインしてもらう -> アカウント認証後、ダウンロードする
```

匿名認証からのアカウント認証への切り替えの実装例は次のとおりです。

```dart

final _auth = FirebaseAuth.instance;

enum LoginType {
  google, facebook, apple,
}

Future<void> loginWithSwitching(LoginType loginType) async {
  // 現在のログイン情報を取得
  final user = await _auth.currentUser();
  if (user == null) {
    // 匿名認証でログインしていることを前提としているのでreturnする
    return;
  }

  // 指定されたLoginTypeのCredentialを取得
  AuthCredential credential;
  switch (loginType) {
    case LoginType.google:
      credential = await _credentialWithGoogle();
      break;
    case LoginType.facebook:
      credential = await _credentialWithFacebook();
      break;
    case LoginType.apple:
      credential = await _credentialWithApple();
      break;
  }

  // 過去に同じアカウントでログインしているか確認, Firestoreから取得
  final isNewAccount = ....

  if (isNewAccount) {
    // 新規アカウントのため linkWithCredential で匿名認証からアカウント認証へ切り替える
    await user.linkWithCredential(credential);

    // 過去にログインしたことを保存するためuserId等をFirestoreへ保存
    ...
  } else {
    // 過去にログインしたことがある場合は普通にログインする
    await _auth.signInWithCredential(credential);
  }
}

```

また、既にログインした後でアプリを再起動しても自動的にアカウントログインした状態でアプリを始めれるようにしています。


### 英作文のダウンロード

Lalaが提供する英作文を使って勉強します。

英作文はSearch画面よりダウンロードできます。今後、さまざまなシチュエーションの英作文はたくさん追加していく予定です。

<img width="375" alt="IMG_0623.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/d3eff5f4-c9ac-5870-0b55-a0d9690a0274.png">

ダウンロードすると学習できます。

<img width="375" alt="IMG_0626.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/036c9fb9-4cf3-aa89-22e2-3502c06f95c3.png">

また、ホーム画面に一覧表されます。

アプリを開いてすぐにダウンロードした英作文で学習できるように工夫しました。

<img width="375" alt="IMG_0625.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/4d7d57e8-45a9-0649-4521-803e44603921.png">

#### ダウンロード処理のポイント

英作文のダウンロード機能で利用するライブラリは次のとおりです。

- [flamingo](https://pub.dev/packages/flamingo)
- [cloud_functions](https://pub.dev/packages/cloud_functions)
- [path_provider](https://pub.dev/packages/path_provider)

flamingoはFirestoreのモデルフレームワークライブラリです。詳細はこちらを参考にしてください。
[FlutterのFirebase Firestoreモデルライブラリを作った話（Flamingo）](https://qiita.com/hukusuke1007/items/79ffdae9c832af917348)

ダウンロードできる英作文はマスターデータとして予めFirestoreへインポートしています（インポートスクリプトは自前で実装しています）。

インポートする英作文のデータ構造は次のとおりです。

```
// 英作文のタイトルなどの看板データ
/script/:script_id
```

```json
{
  "uid": "scriptId",
  "title": {
    "japanese": "タイトル",
    "english": "title"
  },
  ...
}
```

```
// 英作文データ
/script/:script_id/sentences/:sentence_id
```

```json
{
  "uid": "sentenceId",
  "scriptId": "scriptId",
  "order": 0,
  "text": {
    "japanese": "こんにちは",
    "english": "Hello"
  }
  ...
}
```

ダウンロード機能の処理は次のとおりです。

1. ダウンロードするscriptのスナップショットを保存
2. ダウンロードカウントを上げる
3. 英作文（sentences）をローカルに保存（アプリ側）

学習するスクリプトとして、scriptのスナップショット等をuserのSub collectionへ保存します。データ構造は次のとおりです。

```
// 学習するスクリプト
/user/:user_id/study_scripts/:study_script_id
```

```json
{
  "studyCount": 0,
  "score": 0,
  "memo": "メモ",
  "scriptId": "scriptId",
  "title": {
    "japanese": "タイトル",
    "english": "title"
  },
  ...
}
```

1, 2.のダウンロード処理はCloud Functionsで行います。理由は今後有料スクリプトを提供することを考えているためです。

LINEスタンプのようにアプリ内課金によりアプリ内通貨を獲得し、その通貨で有料スクリプトをダウンロードする機能を検討しています。これは将来的に<b>ユーザーが作った英作文を売買できるようにするため</b>です。

課金に関わる実装はリスクが高いためサーバー側で行います。今は有料スクリプトはありませんがこの段階でサーバー側で実装させて今後に備えています。

3.に関しては、Firestoreの取得回数を少なくするためダウンロードのタイミング（又は学習画面遷移後にローカルにデータがない場合）でローカルへ保存し、学習時にローカルから英作文を取得しています。
ローカルディレクトリの操作はpath_providerを使用しています。Firestoreのキャッシュ機能があるんですが、キャッシュ期間がいまいち掴めていないので今はこのように実装しています。

scriptとsentencesのマスターデータが更新された場合は、ユーザーに促しアップデートしてもらうことで最新の英作文を取り込めるようにしています。

#### 学習画面の実装ポイント

学習をスタートすると一定の時間感覚で英作文が表示されます。次のライブラリを使っています。

- [stop_watch_timer](https://pub.dev/packages/stop_watch_timer)

実装イメージです（全て書くと長くなるのでかなり省略します）。

```dart
final StopWatchTimer _countTimer = StopWatchTimer();

_countTimer
  .secondTime
  .listen((d) {
    // secondTimeをlistenすると１秒毎にここのロジックが実行される
    // 出題文（又は回答文）の時間をカウントダウンして0になると画面上にメッセージ表示する処理を実行
    ...
});


// 終わったらdispose忘れないこと
_countTimer.dispose();
```

メッセージ枠のWidgetは次のライブラリを使っています。このぐらいであれば自作した方が良い気がしてます。

- [bubble](https://pub.dev/packages/bubble)

メッセージが表示されると自動的に下へスクロールするよう次のような実装を入れています。

```dart
final _scrollController = ScrollController();

// メッセージの表示より早くスクロールされる場合があるため、delayedを入れて少し遅延させている
Future.delayed(const Duration(milliseconds: 300), () {
  _scrollController.animateTo(
      _scrollController.position.maxScrollExtent,
      duration: const Duration(milliseconds: 200),
      curve: Curves.easeOut,
  );
});
```

画面の上に戻るときは次のような実装です。

```dart
_scrollController.animateTo(
    _scrollController.position.minScrollExtent,
    duration: const Duration(milliseconds: 10),
    curve: Curves.easeOut,
);
```

### オリジナル英作文の作成

Lalaはオリジナルの英作文も作れます。

<img width="375" alt="IMG_0627.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/23e332a8-a50c-dbfc-3d4c-0a3b6c4dcd68.png">

<img width="375" alt="IMG_0628.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/675f76d5-da67-d4af-843e-9dba6a64834d.png">

作成した英作文は、ダウンロードした英作文と同様に学習できます。

<img width="375" alt="IMG_0629.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/0e38b06a-5e83-a98c-6180-38d0a89f4ce4.png">

将来的には作成した英作文をユーザー間で共有する機能を実装する予定です。


#### 実装ポイント
作成した英作文はFirestoreへ保存しています。

作成した英作文はuserのSub collectionとして管理しています。なお、ユーザー作成の場合はsentencesの数を有限にしているので、配列として扱うようにしています。データ構造は次のとおりです。

```
// 作成した英作文
/user/:user_id/user_scripts/:user_script_id
```

```json
{
  "scriptId": "originalScriptId",
  "title": {
    "japanese": "タイトル",
    "english": "title",
  },
  ...
  "sentences": [
    {
      "order": 0,
      "text": {
        "japanese": "餃子について",
        "english": "About a Gyoza."
      },
    },
    {
      "order": 1,
      "text": {
        "japanese": "おいしいです",
        "english": "It's delicious."
      },
    },
    ...
  ],
}
```

作成と同時に、学習するスクリプトとしても管理します。

```
// 学習するスクリプト
/user/:user_id/study_scripts/:study_script_id
```

```json
{
  "studyCount": 0,
  "score": 0,
  "memo": "メモ",
  "scriptId": "originalScriptId",
  "title": {
    "japanese": "餃子",
    "english": "Gyoza"
  },
  ...
}
```

画像はCloud Storageへ保存しています。

flamingoが提供するStorageを使えばCloud Storageへ画像を保存した後にそのURLをFirestoreへ保存してくれます。
[flamingo#File](https://github.com/hukusuke1007/flamingo#File)

### 学習結果の保存

スコア、メモ、学習回数を学習記録として保存します。

<img width="375" alt="IMG_0630.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/87cd5cb9-46e3-ce94-e8fa-e68a3e78a777.png">

日々の学習記録はRecord画面より確認できます。

学習意欲を向上させるため、学習回数などは年月単位でグラフとして確認できます。

<img width="375" alt="IMG_0632.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/617ec584-f744-f109-90dd-da44187fe17c.png">


#### 実装ポイント
学習した英作文のスコアやメモ等は前述したSub collectionで管理しています。

毎日の学習回数は別のSub collectionで、年間365日分の回数を1つのドキュメントで管理しています。

```
// 学習回数
/user/:user_id/study_records/:study_record_id
```

```json
{
  "jan": {
    "firstDayCount": 0,
    ...
    "thirtyFirstDayCount": 0,
    "totalCount": 0
  },
  "feb": {
    "firstDayCount": 0,
    ...
    "thirtyFirstDayCount": 0,
    "totalCount": 0
  },
  ...
  "dec": {
    "firstDayCount": 0,
    ...
    "thirtyFirstDayCount": 0,
    "totalCount": 0
  }
}
```

このデータ構造で問題ないか[割り当てと上限](https://cloud.google.com/firestore/quotas?hl=ja)を確認し、実際に365日のデータをフルで入れてみ動作確認したところ問題なく動いていたのでこのデータ構造で動かしています。

グラフは次のライブラリを使っています。

- [fl_chart](https://pub.dev/packages/fl_chart)

[こちらのドキュメント](https://github.com/imaNNeoFighT/fl_chart/blob/master/repo_files/documentations/line_chart.md)から実装例を確認できます。

### アプリ内課金

Lalaはアプリ内課金によりさらに学習できるよう機能をグレードアップすることができます。

フリープランとスタンダードプランの違いは次のとおりです。

|  | フリープラン |スタンダードプラン |
|---|---|---|
|広告  |表示 |<b>非表示</b> |
|英作文の作成回数  |3  |<b>10</b> |

<img width="375" alt="IMG_0633.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/dd31f95d-f5cd-9743-e6a3-e02be662a99b.png">

#### 実装ポイント
Flutter公式が提供しているアプリ内課金ライブラリを使っています。

- [in_app_purchase](https://pub.dev/packages/in_app_purchase)

アプリ側の実装はこのライブラリの[example](https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/example)を参考にすれば良いのですが、サブスクリプションのアプリ内課金を実装する場合は<b>レシート検証のサーバー</b>を用意しなければいけません。

LalaではCloud Functionsで実装しています。

課金後に発行されるレシートを検証し、取得した有効期限からプランの状況を確認する必要があります。レシート検証用サーバーの実装はGooglePlay, Appleそれぞれ用意しなければいけません。

また、テストは必須（そういう理解）なのでテスト用のプロダクトIDが必要になります。そのため、開発用と本番用でビルド設定を切り分ける必要があります。
ビルド設定を切り分ける方法はこちらの記事を参考にしてください。
[Flutterで環境ごとにビルド設定を切り替える — iOS編](https://medium.com/flutter-jp/flavor-b952f2d05b5d)
[flutterで本番/ステージング/開発を切り替える](https://qiita.com/ko2ic/items/53f97bb7c28632268b5a)

詳細を書くとかなりのボリュームになるため進め方と実装する上で参考にした記事を紹介します。

#### iOS

手順は次のとおりです。

```
1. iTunes Connectにアプリのプロダクト登録
   テスト環境も同様に作成する、プロダクトIDは本番とテスト用の二つ必要
2. サブスクリプションアイテムを登録（開発, 本番環境ともに）
3. iTunes Connectでテスト用のSandboxユーザーを作成
4. レシート検証APIを実装する（サーバー側の実装）
```

サーバー側の実装はこちらの記事を参考にしました。
[自動購読課金について【iOS編】](https://ameblo.jp/principia-ca/entry-12071724382.html?frm=theme)
[【iOS/Android】アプリ内課金 定期購読のサーバーサイド知識総まとめ](https://qiita.com/ckm/items/b8cf23ba4bd0ae5bbf34#ios)

AppleのAPIから取得できるデータ構造は次の記事でまとめられています。
[Apple月額課金について【Productionレシート】](https://qiita.com/econa77/items/1f653ff6e0ab151a6eae)

#### Android

手順は次のとおりです。

```
1. GooglePlay ConsoleからGCPのOAuthクライアントとサービスアカウントのリンク付けを行う
   [GooglePlay Console] -> [デベロッパーアカウント] -> [APIアクセス] 
2. GooglePlayにアプリのプロダクト登録
   テスト環境も同様に作成する、プロダクトIDは本番とテスト用の二つ必要
3. サブスクリプションアイテムを登録（開発, 本番環境ともに）
4. レシート検証APIで使うリフレッシュトークンを取得
5. レシート検証APIを実装する（サーバー側の実装）
```

レシート検証APIで使うリフレッシュトークンの取得方法はこのドキュメントに書かれています。
https://developers.google.com/android-publisher/authorization
※redirect_uriは http://localhost にすると確認できました。


サーバー側の実装はこちらの記事を参考にしました。
[【iOS/Android】アプリ内課金 定期購読のサーバーサイド知識総まとめ](https://qiita.com/ckm/items/b8cf23ba4bd0ae5bbf34#android)

GooglePlayのAPIから取得できるデータ構造はこのドキュメントに書かれています。
[Purchases.subscriptions](https://developers.google.com/android-publisher/api-ref/purchases/subscriptions?hl=ja)

##### Androidのハマりポイント

1.を行う前にサブスクリプションアイテムを登録しないでください。サービスアカウントとリンクする前に登録したアイテムで確認したところ、有効でないと判断され動きませんでした。

一度登録した課金アイテムは削除できないので、プロジェクトを作り直すか又は別の新しいアイテムを作るしかありません。これが本番環境だと無駄に使われないアイテムが並ぶことになるので注意してください。
[１度有効にした定期購入アイテムは２度と無効にすることはできない、絶対に、如何なる理由や事情があっても……だ！（Google Play 課金）](https://qiita.com/84d010m08/items/ed116296bfd21f26e0ee)


## UI/UXデザイン

UI/UXデザインは[Dribbble](https://dribbble.com/)でコンセプトにあいそうなもの見つけてそれを参考にしました。

https://dribbble.com/shots/7365750-Mobile-App-Craft-Store 
https://dribbble.com/shots/6654840-Magazine-Store
https://dribbble.com/shots/7299028-Application-For-Amateur-Musicians-4
https://dribbble.com/shots/7226402-Dating-App-Concept
https://dribbble.com/shots/4874998--Exploration-Gubooks

また、[FacebookのThreads](https://about.fb.com/ja/news/2019/10/threads/)というアプリも参考にしています。

アプリのデザインが決まればワイヤーフレームで各画面を作りました。

開発は自分だけなのでワイヤーフレームは手書きです。

![IMG_0642.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183031/3fe83d43-808d-9577-b90b-1b4cc22fb3dd.jpeg)

すごい適当に見えますが真面目に考えて書いています笑。

ペンと紙があれば大丈夫なので、低コストでデザインできます。

## ユーザーレビュー

7人の方にα版でユーザーテストをしていただきました。

| 名前 | 年代|性別 |
|---|---|---|
|Aさん  |20代 |男性|
|Bさん  |30代 |女性|
|Cさん  |20代 |男性|
|Dさん  |30代 |男性|
|Eさん  |60代 |男性|
|Fさん  |20代 |女性|
|親父  |60代  |男性 |

そこで出たフィードバックと対応策はこちらです（一部）。


| コメント | 対応策|
|---|---|
| ホーム画面がわからない | 初回起動時にモーダルでダウンロード画面を表示していた仕様を排除。ダウンロード画面にホーム画面のアイコン設置して押すとホーム画面に戻るように対応|
| 学習画面遷移後に表示されるダイアログのスタートボタンがわかりづらい | スタートボタンの色を変更　|
| 初回起動後、どこをタップすればダウンロード画面に遷移するのかわからない | 「ここをタップしてダウンロード画面...」の説明文をホーム画面上に表示。タップするとダウンロード画面に遷移するよう対応|
| アプリを使うのにログインが必要ということがわからない | ログインモーダルに説明文とボタンに各アカウントアイコンを設置|
| ダウンロードした後、どうすれば良いかわからない | 英作文のダウンロード画面でStartボタンを設置して、押下すると学習画面へ遷移するように対応 |

ユーザーレビューは必須ですね。かなり改善されました。

## マーケティング

無事リリースできても使ってもらわなければ意味がありません。

使ってもらえれるようアプリのマーケティングにも力を入れないといけません。

アプリをダウンロードして毎日使ってもらうためにはどうすれば良いのか簡潔に紹介します。なお、個人開発なのでお金をかけないやり方で進めます。

1. 周りの友人や知人に使ってもらう
2. Product Huntでアプリを投稿する
3. TwitterやFacebookなどのSNSで公表

### 周りの友人や知人に使ってもらう

その名の通り、周りの方に~~無理やり~~ダウンロードしてもらい使ってもらいました。


### Product Huntでアプリを投稿する

開発したプロダクトをアナウンスできるサービスですね。投票機能があり、投票されると結構みられたりするみたいです。
https://www.producthunt.com/

こちらはただポストしているだけなので力を入れていきたいと思います。
https://www.producthunt.com/posts/lala-study-english-japanese

進捗ありましたら追記していきます。

### TwitterやFacebookなどのSNSで公表
SNSは本当に良いマーケティングツールだと思います。自分はTwitterしか力を入れていないのでTwitterで公表しました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">英語の瞬間翻訳トレーニングアプリ Lala をリリースしました！<br><br>出題された文章を英語に翻訳（脳内翻訳）し学習していくアプリです。回答例も表示されるので翻訳した内容と確認できます。<br><br>無料なのでぜひ使ってみてくださいm(__)m<br><br>・iOS<a href="https://t.co/dXUZJLPjCr">https://t.co/dXUZJLPjCr</a><br><br>・Android<a href="https://t.co/gL0zKXArGa">https://t.co/gL0zKXArGa</a></p>&mdash; shohei@英語の瞬間翻訳トレーニングアプリ Lala (@hobbydevelop) <a href="https://twitter.com/hobbydevelop/status/1233217890133790720?ref_src=twsrc%5Etfw">February 28, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

たくさんいいねをいただきました。本当にありがとうございますm(__)m

### アクティブユーザー

Firebase Analyticsを導入して計測しています。

- [firebase_analytics](https://pub.dev/packages/firebase_analytics)

1ヶ月毎に書いていきたいと思います（追記予定）。

もう少々お待ちください。

## その他

### Flutterライブラリの紹介
[【オンライン開催】Flutter Meetup Osaka #2](https://flutter-jp.connpass.com/event/159013/)で使ってみたFlutterのライブラリの紹介をしました。その時の資料です、ぜひ参考にしてみてください。
[Flutter開発の中で使ったライブラリたちの紹介(Keynoteファイル)](https://drive.google.com/open?id=105t4Llif_w8PnO00g6eU6LPAFuB6KYap)

※動画を入れているので重たいです。

### Flutter開発でよく使うシェル

```sh

# 接続されているデバイス一覧
flutter devices

# ビルド実行, $1は flutter devices で表示されているデバイスIDを指定
flutter run --debug --flavor development -d $1 --target lib/main_development.dart

# アプリアイコンの作成（flutter_launcher_iconsプラグインが必要）
flutter pub run flutter_launcher_icons:main

# Androidに必要なkeystoreを生成(Debug用）, $1はkeyAlias名として指定する（自分が決める）
keytool -genkey -v -keystore android/debug.keystore -alias $1 -keyalg RSA -validity 10000 -dname "CN=Android Debug,O=Android,C=JA"
keytool -list -v -alias $1 -keystore android/debug.keystore
keytool -exportcert -alias $1 -keystore android/debug.keystore | openssl sha1 -binary | openssl base64

# Androidに必要なkeystoreを生成(Release用）, $1はkeyAlias名として指定する（自分が決める）
keytool -genkey -v -keystore android/release.keystore -alias $1 -keyalg RSA -validity 10000
keytool -list -v -keystore android/release.keystore
keytool -exportcert -alias $1 -keystore android/release.keystore | openssl sha1 -binary | openssl base64

# AndroidのRelease用バイナリ生成、GooglePlayにリリースするために必要
flutter build appbundle --release --flavor production --target lib/main_production.dart
```

## 終わりに

長々と書きましたが、今後も追加機能や不具合修正など行いグレードアップしていきます。

ぜひダウンロードしてみてください。

・iOS
[https://apps.apple.com/jp/app/id1493691416](https://apps.apple.com/jp/app/id1493691416)

・Android
[https://play.google.com/store/apps/details?id=com.gmail.hukusuke1007.lala](https://play.google.com/store/apps/details?id=com.gmail.hukusuke1007.lala)