【Flutter】17歳が一人でモバイルアプリを開発・リリースするまでの話 - Zikanri

こんにちは。
**18歳**です。(タイトル詐欺です。)

今回初めて一人でアプリを作ったので、開発の**きっかけ**や**知見**を共有しようかと思い、記事を書きました。

アプリを作ったのは17歳の時なので、この記事の中では私を**華のセブンティーン**として扱ってください。
（実はこれ以上、年齢の話は出てきません）

記事がおもしろかったり、参考になったよという人は、
記事の評価や**twitter**([@thoth000](https://twitter.com/thoth000))のフォローをしてもらえると、とてもうれしいです。
次の記事作成のモチベーションになります。

それでは記事、始まります。

#開発したアプリ
<img width=20% alt="zikanri_icon" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/ec511005-fe9c-3aa7-f79e-d2fa284307e8.png">
<img width=80% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/f43239d4-4c1c-6efb-a486-16dca6e618a9.png">


アプリの名前は**Zikanri**です。

**価値アリ**と**価値ナシ**の二種類の時間だけに限定して記録するアプリです。

記録を限定することで
**毎日の価値**がどれだけなのかがすぐに分かります！

価値アリ・価値ナシでもない「**行方不明な時間**」も見つけてあげることができます。

記録をSNSにシェアして、毎日の価値の変化を共有できるので、モチベーションを維持できます！

* 休みの日についダラダラしてしまう人
* 目標に向かって頑張る人

など、毎日の価値を改善したい人に使ってもらうことで、
時間を振り返って**毎日をより良くする**モチベーションを生み出します！

ダウンロードはこちらから！
ぜひレビューをお願いします！

<a href='https://play.google.com/store/apps/details?id=com.thoth000.zikanri_app&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Google Play で手に入れよう' src='https://play.google.com/intl/ja/badges/static/images/badges/ja_badge_web_generic.png'/></a>

#目次
[開発したアプリ](#開発したアプリ)
[アプリ開発を始めた経緯](#アプリ開発を始めた経緯)
[開発の流れ](#開発の流れ)
[Flutterについて](#flutterについて)
[開発の知見](#開発の知見)
　[ChangeNotifier](#changenotifier)
　[Widgetの画像化](#widgetの画像化)
　[シェア機能](#シェア機能)
　[HiveというDB](#hiveというdb)
　[BottomNavigation](#bottomnavigation)
　[(初心者でもわかる)ダークモード](#ダークモード)
　[チュートリアルの実装](#チュートリアルの実装)
　[キーボードを開くときの工夫](#キーボードを開くときの工夫)
　[Rowの両端寄せの罠](#rowの両端寄せの罠)
　[TextFieldの最大文字数](#textfieldの最大文字数)
　[SVGファイルの表示](#svgファイルの表示)
　[ファイルの整理](#ファイルの整理)
　[アプリ公開のスクリーンショット](#アプリ公開のスクリーンショット)
　[アプリのリリースビルド](#アプリのリリースビルド)
[大事だと思う話](#大事だと思う話)
[さいごに](#さいごに)

#アプリ開発を始めた経緯
まずアプリ開発を始めた理由はなにかというと

**コンテストに参加するために始めました。**

毎年行われているアプリ甲子園というコンテストがあります。
そのコンテストに去年(2019)、先輩から誘われたということで年上だけのグループに一人ちょこんと参加したのが始まりです。

**Flutter**というクロスプラットフォームを使って開発をしました。
コンテストは残念な結果になってしまったので、今年は先輩のリベンジ的な立ち位置でひとり頑張っています。
応援していただけると嬉しいです。

先輩方とコンテストに参加し終わったあと、個人的にスキルの修得に力を入れ始めて、個人開発という流れになりました。

今回開発したアプリもこんな流れでFlutterを使って作っています。

#開発の流れ
開発期間は半年ほどです。
アイデアは登校中のバスの中とかで思いついたと思います。
一時期、「なんかアプリ作りたいな」と猛烈に思っていた時期があって、ぼーっとした顔で明後日の方向を見ながら、暇な時に考えていたと思います。

(Androidは)18歳にならないとアプリリリースができないことを知っていたので、のんびりと開発を進めていきました。

今年の春休みにギアを上げて無事完成させました。
（リリースは夏です。）

#Flutterについて
そもそもFlutterとはクロスプラットフォームに対応したアプリ開発キットです。
`Google`さんが開発をしています。

キットというと語弊が生じるかもしれませんが、`Dart`というプログラミング言語を用いて、アプリのフロントエンドからバックエンドまで設計していきます。

公式のドキュメントがしっかりしているので、ここを見ればある程度のアプリは作れるようになると思います。
（Flutterの情報を仕入れるときは、基本的に英語のドキュメントを読むことになると思います。翻訳でも理解できるかな？…と思います。）
[Flutter](https://flutter.dev/)

またサードパーティ製のパッケージが豊富で、開発にとても便利に働きます。
[Dart Packages](https://pub.dev/)

#開発の知見
ここからは実装の話になります。
Flutterを知らなくても興味を持ってくれた方は、学んでからこの記事をまた読んでくれるとうれしいです。（[大事だと思う話に飛びます。](#大事だと思う話)）
開発で詰まった箇所や良さそうなウィジェット、パッケージを紹介しようと思います。
個人の知見なので修正点があれば教えていただけるとありがたいです。（もちろん修正します。）

#ChangeNotifier
値の変更をピンポイントでウィジェットに伝えるために使いました。
Class 〇 with ChangeNotifierで定義して使うことができます。

`ChangeNotifier`（以下CNとします）は`ChangeNotifierProvider`(以下CNP)の子クラスで使うことができます。

自分はCNを複数用いることにしたので、CNPのネストが深くなってしまいそうですが、そこは`MultiPlovider`で解決できます。

```dart:BAD
ChangeNotifierProvider(
  create: (_) => CN1(),
  child: ChangeNotifierProvider(
    create: (_) => CN2(),
    child: ChangeNotifierProvider(
      create: (_) => CN3(),
      child: child,
    ),
  ),
),
```
悪い例です。
CNPのネストには悪い部分が二つあると思ってます。

一つ目は**ダサい**です。そのままの意味です。
二つ目はこれが原因でスプラッシュ画面の後に一瞬**ブラックスクリーン**になることがあります。私はこの原因解明にかなりの時間を費やしました…
きれいにアプリ起動ができなくなるので注意しましょう。


```dart:GOOD
MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => CN1(),),
    ChangeNotifierProvider(create: (_) => CN2(),),
    ChangeNotifierProvider(create: (_) => CN3(),),
  ],
  child: child,
),
```
`MultiProvider`を使うことで、きれいに実装できています。

#Widgetの画像化
<img width=80% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/16b807fe-da56-ac25-6a3e-4b62b7b68f94.jpeg" alt="screenshot">


Flutterでは`Widget`を画像化することができ、それには`RepaintBoundary`を使います。
画像化したいWidgetを`RepaintBoundary`で囲むことで、画像化できるWidgetを作ります。

```dart
RepaintBoundary(
  key: _globalKey, //グローバルキーを割り当てます
  child: child,
),
```

画像化のメソッドはこんな感じです。

```dart
import 'dart:ui' as ui;

Future exportToImage() async{
  RenderRepaintBoundary boundary = 
    _globalKey.currentContext.findRenderObject(); //ここでRepaintBoundaryにあてたキーを参照します。
  ui.Image image = await boundary.toImage(
    pixelRatio: 3.0, //値の変更可能
  );
  ByteData byteData = await image.toByteData(
    format: ui.ImageByteFormat.png,
  );
  final _pngBytes = byteData.buffer.asUint8List(); //生成された画像
}
```

#シェア機能
<img width=35% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/0a3e7917-532f-4bd7-cc4c-22f632826893.png",alt="shere">  <img width=35% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/02cf147d-5ae1-fdaa-d492-41d2d0879183.png" alt="twitter">

シェアするにはさまざまな方法がありますが、自分は`esys_flutter`を採用しました。

`esys_flutter`は画像もシェアできるので、上の`RepaintBoundary`と組み合わせてウィジェットの画像をシェアすることもできます。便利ですね。

```dart
await Share.file(
  'title','image.png',_pngBytes,'image/png',text:'シェアする文章',
);
```

Shere.fileの中身は左から

|箇所|説明|
|:--|:--|
|タイトル　|シェアのタイトルです。シェアの動きには特に関係ないので、アプリのタイトルやシェアの内容を記入しましょう。|
|ファイル名　|メールなどで共有するとき、ファイルの名前を指定できます。|
|ファイル　|シェアするファイルです。|
|ファイル形式　|（ここが大事です。）image/pngとすることでtwitterなどでシェアできるようになります。|
|テキスト　|シェアする文章です。|
シェアするときは、左の画像のようなメニューが出てきます。

#HiveというDB
<img width=50% src="https://raw.githubusercontent.com/hivedb/hive/master/.github/logo_transparent.svg?sanitize=true">

**注意**
`Hive`のversion 2.0で前バージョンと互換性がなくなるという話が出ています。
それを理解したうえでお読みください。

`Hive`とは軽量で高速な`NoSQL`のDBです。

`SharedPreference`や`SQLite`と比較しても**パフォーマンスが高い**ようです。
データはkey-valueで保存できます。

使い方を大まかに記します。
まずは以下をpubspec.yamlに通します。
バージョンは更新されるので、各自確認してください。

* **hive**
* **hive_flutter**
* **hive_generator**

`Hive`は`box`というデータベース概念があります。
`box.openBox()`でボックスを開くとデータベースにアクセスできるようになります。

**ここで注意です。**
`box`のディレクトリはスマホのどこにでも自由に作っていいということは絶対にありえないので、適切な場所に`box`のアクセスができるようにする必要があります。
それをしてくれるのが`Hive.initFlutter()`です。
`box.openBox()`をする前に絶対にこの関数を実行しましょう。

```dart
await Hive.initFlutter(); //awaitはつけましょう。
await Hive.openbox('dog'); //dogという名前のboxを開きます。 
dog_box = Hive.box('dog'); //dog_boxが名前dogのboxになります。
```

boxに対するアクションを説明します。

|関数名|アクション|
|:--|:--|
|box.put('key',value)|boxを指定して、key-valueで値を保存できます。|
|box.get('key')|boxを指定して、そのキーに対応する値を取り出せます。|
|box.clear()|boxを指定して、dataの全クリアができます。|
|box.containsKey('key')|boxを指定して、その中にキーがあるかを調べます。trueかfalseが帰ります。|
|box.delete('key')|boxを指定して、キーを削除できます。|

```dart
dog_box.put('count',3); //countというキーで3を保存します。
dog_box.put('list',[[],[],]) //リストも保存できます。

//put_null と deleteの違いについて
dog_box.put('count',null)
//dog_box.containsKey('count') => true
dog_box.delete('count')
//dog_box.containsKey('count') => false
```

主なものはこの5つになります。
特にawaitがなくても動きます。動作が途中で中断された際は動作前の状態に戻ってくれます。どこまでも賢いですね。（心配な人はawaitをつけてもいいです。）

box.openBox()をすることで、アクションを行えるようになるので気を付けましょう。

最後にアプリを閉じるときに、boxが開いているような状態は意図的でない限り良くないので、`dispose`で閉じるようにしましょう。

また、flutterの現version(stable:1.17.2)では、Hiveに必要な`HiveGenerator`がエラーを吐きます。これを解決する方法は以下の通りです。オーバーライドで対応します。

```yaml:pubspec.yaml
dependency_overrides:
  dartx: ^0.3.0
```
詳しくは
https://github.com/hivedb/hive/issues/247#issuecomment-606838497

#BottomNavigation
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/476ee245-6008-d8e4-4788-ede3b1ca1df9.png">

`BottomNavigationBar`に`FloatActionBotton`を埋め込んだデザインにしてあります。
このデザインにした理由は、どこからでも移動なしで記録が行えるからです。

自分なりの実装方法を紹介します。
`BottomNavigationBar`は使わずに`BottomAppBar`を使います。
childは`Row`の中に`IconButton`を入れました。

```dart
floatingActionButton: floatButton(),
floatingActionButtonLocation:FloatingActionButtonLocation.centerDocked,//BottomBarの中央に埋め込み
bottomNavigationBar:BottonAppBar(
  shape:CircularNotchedRectangle(),
  child:Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children:<Widget>[
      IconButton(),
      ...
      SizedBox(), //FloatActionButtonを埋め込むスペースを空ける
      IconButton(),
      ...
    ],
  ),
),
```

`BottomAppBar`で実装した理由は`BottomNavigationBar`ではタッチ判定なしの空白を用意できなかったからです。（なにかしらやり方があるかと思いますが…）
少しでもユーザーの見えるところは妥協したくなかったので、愚直に実装しました。

#ダークモード
ダークモードは、上の`ChangeNotifier`を使うことで実は超簡単に実装できます。
流れとしては、`ChangeNotifier`内に`ThemeData`を定義して`MaterialApp`で参照することでテーマの切り替えに対応できます。

```dart
Class ThemeNotifier with ChangeNotifier{
  bool _isDark = false;
  //アプリのテーマ
  ThemeData appTheme() => ThemeData(
    brightness: _isDark ? Brightness.dark : Brightness.light;
  );
  //モードの変更
  void changeTheme(){
    _isDark = !_isDark;
    notifyListeners();
  }
}
```
これでダークテーマへの対応ができました。
なんて簡単なんでしょうか。素晴らしいですね。
これを`MaterialApp`に適応させてみましょう。

```dart
MaterialApp(
  title:'Flutter',
  theme:Provider.of<ThemeNotifier>(context).appTheme(),
  home:HomePage(),
),
```
あとは適当なボタンに`changeTheme`の関数を割り当てれば完成です。
これであなたはダークモード実装ができました。
友達に自慢できます。自慢しましょう。

`ThemeData`ではデフォルトのカラーなどの基本スタイルを設定できます。スタイルを部品ごとで定義するのではなくて、一つの場所で定義するとアプリ全体に統一感が出やすくていいと思います。


#チュートリアルの実装
アプリを使い方というのは、ユーザーは何も知らないわけなのでそれを説明する必要があります。ですが、説明はなるべく少なく直感的にユーザーが使えるようなアプリがいいアプリなのかなと私は思っています。

Zikanriは記録アプリなので、気軽に素早く記録を行える必要がありました。
なので、どうしてもショートカットなどの時短化機能を実装せざるを得ませんでした。
（ネガティブな風に言ってますが、時短化機能はいい機能だと思っています。）

なのでチュートリアルは他のアプリに比べて少し多いかなという印象です。

自分がアプリに求めるようなチュートリアル要素は以下の通りです。

* ユーザーがしっかり理解するまで表示される。
* ユーザーのアクションの邪魔をしない。
* デザインに溶け込む
* チュートリアルらしくしない

上の項目を考慮して、このアプリのチュートリアルについて考えました。

それは**カードとして画面に溶け込ませること**です。

画像で見ると分かりやすいと思います。

<img width=35% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/cc59ce98-5ae4-377c-1aed-1412886823de.png"> <img width=35% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/8df5ca3f-7a32-a04e-7f9d-5acc9eb19320.png">

右上のボタンが押されるまでチュートリアルは表示され続けるので、ユーザーは好きなタイミングでチュートリアルを閉じることができます。

間違って閉じてしまった場合の対処法として、チュートリアルの一覧表示をするページも作っています。

#キーボードを開くときの工夫
多くのアプリでは`TextField`などでキーボードを開くと思いますが、開くときの注意点、そして工夫点を紹介します。

まず、注意点です。
最初にでるエラーの筆頭としてはやはり`Overflow`ですよね。
これは画面をスクロール可能にすることで解消されます。
解消策としては、`ListView`や`SingleChildScrollView`を使うのが一般的だと思います。
このエラーはググることで皆さん解決できていると思います。

次に、工夫点です。
実はうえの注意点を解消するだけでは、ユーザーが満足のいくアプリにはなっていないことが多いです。
理由は、キーボードによって`TextField`が隠れてしまう場合があるからです。
つまり、**入力されている文字が見えない**ことが発生するわけです。
これをどのように解消するかというと、画面下に適切な空間を空けることで解消されます。

`MediaQuery.of(context).viewInsets.bottom`を使います。
これを使うとシステム側で使われるbottomの高さを取得できます。
これが分かればあとは`SizedBox`を置いたり`padding`を使ったりで適切に対処できると思います。
<img width=35% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/09096ac1-fcf7-e6f9-7a3d-ef97bc63f3ed.png"> <img width=35% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/99b0d91d-03dd-6c52-7f86-e4ca456a2a53.png">
画像のようにキーボードを開くだけで、適切な位置に`TextField`を持ってくることができました。
画像は、`BottomSheet`の下に`MediaQuery.of(context).viewInsets.bottom`の高さを持ってくることでできました。
（なので`BottomSheet`の高さが高くなっているように見えます。）

#Rowの両端寄せの罠
　`Widget`を横に並べるときには、たいてい`Row`という`Widget`が使われます。
この`Row`は大変便利です。

- 横の並び方を変えられる
- 横に並べてある高さの違うWidgetに対して、上揃えや中央揃えができる

のふたつの点が好きです。

しかし罠がありました。
それは、`Row`でWidgetを両端寄せにする場合です。
皆さんご存じのとおり、こんな感じで両端寄せが実現できます。

```dart

Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: <Widget>[
    ...
  ],
)
```

両端寄せといっても、言葉通りになるのは`children`のなかの`Widget`が二つの時だけです。
`Widget`が三つ以上になると、両端が端に寄ってあとは均等に並べられます。

しかし先ほども言った通り、罠がありました。
それは`Row`を`child`に持つ`親Widget`がサイズを持たない場合です。

普通に考えれば「サイズがないのに両端寄せなんてありえない」となるはずです。

しかし私は「**エラー文コピペ野郎**」だったので、何も考えずにGoogleさんやその親戚（Google翻訳）に解決策を見つけてもらいました。
Googleさん達は「親Widgetがサイズを持てばいい」という答えをくれました。

確かにそうです。サイズがないのが問題であれば、サイズを持たせればいいという発想です。

しかしそうなると、どのようにサイズを持たせるかという問題になります。
画面サイズ最大でサイズをとると仮定すると、
`Expand`を親として持ったり、`SizedBox`や`Container`に`MediaQuery`で画面サイズを与えたりと方法が思いつきます。

しかし、簡単な方法がありました。
それは`Spacer`という`Widget`を`children`に組み込むことです。

`Spacer`はflexが1に設定されているだけの、いわゆる空間をあけるためだけの`Widget`です。

- 細かくサイズ指定して空間をあけるなら`SizedBox`、
- 最大まで空間を開けたり、比率で空間のサイズを変えたいときは`Spacer`

と使い分けができると思います。

```dart

Row(
  children: <Widget>[
    //左にあるWidget達
    Spacer(),
    //右にあるWidget達
  ],
)
```
こうすることで、もれなく
「左にあるWidget達」は隙間なく左に寄せられ、
「右にあるWidget達」は隙間なく右に寄せられます。

#TextFieldの最大文字数
　文字を入力するときは、多くの場合`TextFeild`を使うと思います。
その中で何かのタイトルやユーザー名を入力してもらうときは、最大文字数を設定したいと考えると思います。
`TextField`にはその機能があるので、実際に実装してみましょう。

```dart
TextField(
  maxLength:30,
);
```
こうすると文字数を30文字に制限できます。
ですが、以下の写真のようになります。
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/76d795a8-cfb2-9b73-1b95-ba1da495cf66.png">
人によっては黄色く囲った部分が邪魔だと思う人がいると思います。
私もその一人です。

　これを除去するには`maxLength`を使わない別の方法で文字数を制限します。
以下の通りです。

```dart
TextField(
  inputFormatters: [
    LengthLimitingTextInputFormatter(30), //30文字に制限
  ],
);
```
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/8008304e-e0a7-9e26-8de1-d1ed5e1b7091.png">

`inputFormatters`ではこれだけでなく数字だけの入力にしたりできます。
またList型なので`TextInputFormatter`型を複数オプションとしてつけることができます。

#SVGファイルの表示
FlutterはデフォルトではSVGファイルをサポートしていないので、`flutter_svg`というパッケージをインポートします。

```yaml

dependencies:
  flutter_svg: ^0.17.4
```
※現在（6/18）の最新バージョンは0.18.0です。

表示はこうします。

```dart
import 'package:flutter_svg/flutter_svg.dart';

SvgPicture.asset(
  'assets/images/image.svg', //画像パス
  width: 200,
  //ほかにも結構オプションがあります
),
```
自分はSVGファイルを**チュートリアル**の画像として使用しました！

`flutter_svg`の詳細は[こちら](https://pub.dev/packages/flutter_svg)から！

#スマホの縦画面固定
自分の作ったアプリは縦画面固定になっていますが、その方法を紹介します。
`flutter 縦画面固定`と調べると、よくこれが出てきます。

```dart
SystemChrome.setPreferredOrientations([
  DeviceOrientation.portraitUp, //縦画面固定にする
]);
```
これを`main.dart`に書いてもいいんですが、ある時に挙動がおかしくなります。
それはアプリを開くときに横画面だった場合です。
`MediaQuery`で画面サイズを取っている場合には、もれなく最悪になります。

他に方法がないかと探しました。
（Androidに限った話だと思いますが、）`AndroidManifest.xml`に以下を追記することでアプリを縦画面固定にすることができます。

```AndroidMainfest.xml

(activityに書く)
android:screenOrientation="portrait"
```
横画面の起動時でも問題ないです。

#ファイルの整理
アプリ画面には関わってこないんですが、ディレクトリやファイルの整理をすることが**かなり**大事だと思っています。

* 開発中に前書いたコードを見に行きたい
* アプリリリース後に修正点が出てきた

このような場合の時どこに目的のものがあるのかが、格段に見つけやすくなるからです。

この画像を見てください。
<img width = 50% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/a995e1eb-a279-0b00-8235-43f0bffcc217.png">

これは自分が2月ごろに書いていたコードの一部です…
（よく私のPCの中で消されずに生存していたものです）

開発当初、自分は先に言ったことが全く頭になく、コード行が4桁に達するまで一つのファイルに書き込んでしまいました…

ここまでくると修正はかなり面倒です。
（fileに分ける度にimport部分でエラーが山ほど出ます…）

こうなると、ファイルの小分けに全くモチベが出なくなります。笑

新しいアプリ案の開発を始めましたが、そこでは積極的にファイルの小分けを進めています。

頭に入れていることといえば、

* ファイル一つにclassはひとつにする（なるべく）
* class名とファイル名を同等に（class名 : MyBag => file名 : my_bag）

くらいです。
ちなみにclass名やファイル名には命名規則があります。

* class : `UpperCamelCase` => 先頭大文字・区切り大文字
* ファイル・ディレクトリ : `lowercase_with_underscore` => 先頭小文字・区切り_
* フィールド : `lowerCamelCase` => 先頭小文字・区切り大文字


あとはファイルの役割ごとに適切なディレクトリを作って振り分けるだけでファイル管理がかなり楽になると思います。

ぜひ気を付けてください。笑

#アプリ公開のスクリーンショット
iOSはAppStore、AndroidはGooglePlayStoreにデザインの優れた多くのアプリが公開されています。
それに見合うように私もアプリ紹介のデザインには力を入れました。
<img width="60%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539025/2eeee9e4-6cfe-56de-3df0-9487533020e9.png">
上の写真のようなスクリーンショットをデザインしました。
右のようなモックアップの素材はこちらからダウンロードしました。

https://facebook.design/devices

iPhoneやPixelなど主要なデバイスが揃っています。

エミュレーターなどでスクリーンショットをとって、レイヤーで重ねると簡単にデバイスに画面をはめ込んだ写真を作れます。

スクリーンショットでアプリのダウンロード数が変化することもあるので、余裕があればデザインに力をいれてもいいと思います。

#アプリのリリースビルド
(Androidのお話です。)
ここで言えることはただ一つです。
**作成したキーはなくさないようにしましょう。**

一応ですが、参考はこちらから
https://flutter.dev/docs/deployment/android
https://qiita.com/Renkon117/items/80d9775eb870416d69bf

#大事だと思う話
アプリ開発に限らず開発全体、もっと範囲を広げて、人生で大切なことは何でしょうか？
私が思うにそれは**自分で調べてみる**ことだと思います。
調べることで新しい知識や発見を得られるからです。

**Flutter**は最近YouTubeにも取り上げられたりして、日本でのコミュニティは拡大しています。
ですが日本語の情報は少ないです。
何と比べて少ないかといえば、それは**英語(*English*)**の情報です。

今回のアプリ開発では、エラーを数えきれないほど吐かれました。
そんなエラーをどうやって解決したか、やはり調べることによってです。

調べるといっても基本的に英語で調べます。
理由はもちろん、日本語の情報が（まだ）少ないからです。
（もちろん日本語の素晴らしい記事もあります。）

日本語で調べたい情報がなくても、英語ならあったという場合がかなり多くありました。
それはエラーに限った話ではなくて、実装したい機能などもあります。

また最新の情報も英語で提供されることがほとんどです。
などなど、英語のドキュメントを読むといいことがたくさんあります。

開発者として調べることは自分でも大事にしていきたいです。

---

皆さん、林先生はご存じですか？
自分は最近、林先生が出してる本を読みました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">読み出しからけっこうおもしろい。<br>夜寝る前の軽い読書で読もう。 <a href="https://t.co/lgLbYtm6JI">pic.twitter.com/lgLbYtm6JI</a></p>&mdash; thoth000 (@thoth000) <a href="https://twitter.com/thoth000/status/1272538050770288640?ref_src=twsrc%5Etfw">June 15, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

その中でエンジニアに共通するような内容が出てきました。それは、

質問は「**意味を考えてすべし**」ということです。

皆さんの身の回りには皆さんよりも「スゴい人」がいると思います。
皆さんそんな方に何も調べずに質問したりしていませんか？

質問の価値が小さいような質問ばかりすると、最悪の場合「こんなことも自分でできないのか」と**呆れられます**。

もちろん「分からなかったらなんでも聞いてね」という方もいると思います。
しかし、少しは自分の力だけで解決できるようになりましょう。
すると「スゴい人」からの評価も上がると思います。

皆さんの周りには「スゴい人」がいるだけでなく、
それを超える「**スゴいインターネット**」があります。

活用しない手はありません。

#さいごに
ここまで読んでいただいてありがとうございました。
今回が初めて書いた記事だったんですが、楽しんでもらえましたか？

最近（執筆：5月）は、やはりコロナで外出をしにくい状況です。
この期間で、自分の時間の使い方を見直す人も多かったと思います。
こんな時だからこそ、**Zikanri**でそんな人たちをサポートして、新しい挑戦の手助けをしたいです。
みなさんの毎日の価値をぜひ高めてください。

またこのアプリは**アプリ甲子園**というコンテストに応募予定です。
アプリ改善のためにレビュー大募集中です！
いい評価も悪い評価も理由をつけてレビューしてくれると大変役立ちます！



コンタクトをとりたい方は、[自分のツイッター](https://twitter.com/thoth000)のDMまでお願いします。

最後までありがとうございました。