こんにちは。
18歳です。(タイトル詐欺です。)
今回初めて一人でアプリを作ったので、開発のきっかけや知見を共有しようかと思い、記事を書きました。
アプリを作ったのは17歳の時なので、この記事の中では私を華のセブンティーンとして扱ってください。
(実はこれ以上、年齢の話は出てきません)
記事がおもしろかったり、参考になったよという人は、
記事の評価やtwitter(@thoth000)のフォローをしてもらえると、とてもうれしいです。
次の記事作成のモチベーションになります。
それでは記事、始まります。
アプリの名前はZikanriです。
価値アリと価値ナシの二種類の時間だけに限定して記録するアプリです。
記録を限定することで
毎日の価値がどれだけなのかがすぐに分かります!
価値アリ・価値ナシでもない「行方不明な時間」も見つけてあげることができます。
記録をSNSにシェアして、毎日の価値の変化を共有できるので、モチベーションを維持できます!
- 休みの日についダラダラしてしまう人
- 目標に向かって頑張る人
など、毎日の価値を改善したい人に使ってもらうことで、
時間を振り返って毎日をより良くするモチベーションを生み出します!
ダウンロードはこちらから!
ぜひレビューをお願いします!
#目次
開発したアプリ
アプリ開発を始めた経緯
開発の流れ
Flutterについて
開発の知見
ChangeNotifier
Widgetの画像化
シェア機能
HiveというDB
BottomNavigation
(初心者でもわかる)ダークモード
チュートリアルの実装
キーボードを開くときの工夫
Rowの両端寄せの罠
TextFieldの最大文字数
SVGファイルの表示
ファイルの整理
アプリ公開のスクリーンショット
アプリのリリースビルド
大事だと思う話
さいごに
#アプリ開発を始めた経緯
まずアプリ開発を始めた理由はなにかというと
コンテストに参加するために始めました。
毎年行われているアプリ甲子園というコンテストがあります。
そのコンテストに去年(2019)、先輩から誘われたということで年上だけのグループに一人ちょこんと参加したのが始まりです。
Flutterというクロスプラットフォームを使って開発をしました。
コンテストは残念な結果になってしまったので、今年は先輩のリベンジ的な立ち位置でひとり頑張っています。
応援していただけると嬉しいです。
先輩方とコンテストに参加し終わったあと、個人的にスキルの修得に力を入れ始めて、個人開発という流れになりました。
今回開発したアプリもこんな流れでFlutterを使って作っています。
#開発の流れ
開発期間は半年ほどです。
アイデアは登校中のバスの中とかで思いついたと思います。
一時期、「なんかアプリ作りたいな」と猛烈に思っていた時期があって、ぼーっとした顔で明後日の方向を見ながら、暇な時に考えていたと思います。
(Androidは)18歳にならないとアプリリリースができないことを知っていたので、のんびりと開発を進めていきました。
今年の春休みにギアを上げて無事完成させました。
(リリースは夏です。)
#Flutterについて
そもそもFlutterとはクロスプラットフォームに対応したアプリ開発キットです。
Google
さんが開発をしています。
キットというと語弊が生じるかもしれませんが、Dart
というプログラミング言語を用いて、アプリのフロントエンドからバックエンドまで設計していきます。
公式のドキュメントがしっかりしているので、ここを見ればある程度のアプリは作れるようになると思います。
(Flutterの情報を仕入れるときは、基本的に英語のドキュメントを読むことになると思います。翻訳でも理解できるかな?…と思います。)
Flutter
またサードパーティ製のパッケージが豊富で、開発にとても便利に働きます。
Dart Packages
#開発の知見
ここからは実装の話になります。
Flutterを知らなくても興味を持ってくれた方は、学んでからこの記事をまた読んでくれるとうれしいです。(大事だと思う話に飛びます。)
開発で詰まった箇所や良さそうなウィジェット、パッケージを紹介しようと思います。
個人の知見なので修正点があれば教えていただけるとありがたいです。(もちろん修正します。)
#ChangeNotifier
値の変更をピンポイントでウィジェットに伝えるために使いました。
Class 〇 with ChangeNotifierで定義して使うことができます。
ChangeNotifier
(以下CNとします)はChangeNotifierProvider
(以下CNP)の子クラスで使うことができます。
自分はCNを複数用いることにしたので、CNPのネストが深くなってしまいそうですが、そこはMultiPlovider
で解決できます。
ChangeNotifierProvider(
create: (_) => CN1(),
child: ChangeNotifierProvider(
create: (_) => CN2(),
child: ChangeNotifierProvider(
create: (_) => CN3(),
child: child,
),
),
),
悪い例です。
CNPのネストには悪い部分が二つあると思ってます。
一つ目はダサいです。そのままの意味です。
二つ目はこれが原因でスプラッシュ画面の後に一瞬ブラックスクリーンになることがあります。私はこの原因解明にかなりの時間を費やしました…
きれいにアプリ起動ができなくなるので注意しましょう。
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CN1(),),
ChangeNotifierProvider(create: (_) => CN2(),),
ChangeNotifierProvider(create: (_) => CN3(),),
],
child: child,
),
MultiProvider
を使うことで、きれいに実装できています。
FlutterではWidget
を画像化することができ、それにはRepaintBoundary
を使います。
画像化したいWidgetをRepaintBoundary
で囲むことで、画像化できるWidgetを作ります。
RepaintBoundary(
key: _globalKey, //グローバルキーを割り当てます
child: child,
),
画像化のメソッドはこんな感じです。
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(); //生成された画像
}
シェアするにはさまざまな方法がありますが、自分はesys_flutter
を採用しました。
esys_flutter
は画像もシェアできるので、上のRepaintBoundary
と組み合わせてウィジェットの画像をシェアすることもできます。便利ですね。
await Share.file(
'title','image.png',_pngBytes,'image/png',text:'シェアする文章',
);
Shere.fileの中身は左から
箇所 | 説明 |
---|---|
タイトル | シェアのタイトルです。シェアの動きには特に関係ないので、アプリのタイトルやシェアの内容を記入しましょう。 |
ファイル名 | メールなどで共有するとき、ファイルの名前を指定できます。 |
ファイル | シェアするファイルです。 |
ファイル形式 | (ここが大事です。)image/pngとすることでtwitterなどでシェアできるようになります。 |
テキスト | シェアする文章です。 |
シェアするときは、左の画像のようなメニューが出てきます。 |
注意
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()
をする前に絶対にこの関数を実行しましょう。
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を指定して、キーを削除できます。 |
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
がエラーを吐きます。これを解決する方法は以下の通りです。オーバーライドで対応します。
dependency_overrides:
dartx: ^0.3.0
詳しくは
https://github.com/hivedb/hive/issues/247#issuecomment-606838497
BottomNavigationBar
にFloatActionBotton
を埋め込んだデザインにしてあります。
このデザインにした理由は、どこからでも移動なしで記録が行えるからです。
自分なりの実装方法を紹介します。
BottomNavigationBar
は使わずにBottomAppBar
を使います。
childはRow
の中にIconButton
を入れました。
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
で参照することでテーマの切り替えに対応できます。
Class ThemeNotifier with ChangeNotifier{
bool _isDark = false;
//アプリのテーマ
ThemeData appTheme() => ThemeData(
brightness: _isDark ? Brightness.dark : Brightness.light;
);
//モードの変更
void changeTheme(){
_isDark = !_isDark;
notifyListeners();
}
}
これでダークテーマへの対応ができました。
なんて簡単なんでしょうか。素晴らしいですね。
これをMaterialApp
に適応させてみましょう。
MaterialApp(
title:'Flutter',
theme:Provider.of<ThemeNotifier>(context).appTheme(),
home:HomePage(),
),
あとは適当なボタンにchangeTheme
の関数を割り当てれば完成です。
これであなたはダークモード実装ができました。
友達に自慢できます。自慢しましょう。
ThemeData
ではデフォルトのカラーなどの基本スタイルを設定できます。スタイルを部品ごとで定義するのではなくて、一つの場所で定義するとアプリ全体に統一感が出やすくていいと思います。
#チュートリアルの実装
アプリを使い方というのは、ユーザーは何も知らないわけなのでそれを説明する必要があります。ですが、説明はなるべく少なく直感的にユーザーが使えるようなアプリがいいアプリなのかなと私は思っています。
Zikanriは記録アプリなので、気軽に素早く記録を行える必要がありました。
なので、どうしてもショートカットなどの時短化機能を実装せざるを得ませんでした。
(ネガティブな風に言ってますが、時短化機能はいい機能だと思っています。)
なのでチュートリアルは他のアプリに比べて少し多いかなという印象です。
自分がアプリに求めるようなチュートリアル要素は以下の通りです。
- ユーザーがしっかり理解するまで表示される。
- ユーザーのアクションの邪魔をしない。
- デザインに溶け込む
- チュートリアルらしくしない
上の項目を考慮して、このアプリのチュートリアルについて考えました。
それはカードとして画面に溶け込ませることです。
画像で見ると分かりやすいと思います。
右上のボタンが押されるまでチュートリアルは表示され続けるので、ユーザーは好きなタイミングでチュートリアルを閉じることができます。
間違って閉じてしまった場合の対処法として、チュートリアルの一覧表示をするページも作っています。
#キーボードを開くときの工夫
多くのアプリではTextField
などでキーボードを開くと思いますが、開くときの注意点、そして工夫点を紹介します。
まず、注意点です。
最初にでるエラーの筆頭としてはやはりOverflow
ですよね。
これは画面をスクロール可能にすることで解消されます。
解消策としては、ListView
やSingleChildScrollView
を使うのが一般的だと思います。
このエラーはググることで皆さん解決できていると思います。
次に、工夫点です。
実はうえの注意点を解消するだけでは、ユーザーが満足のいくアプリにはなっていないことが多いです。
理由は、キーボードによってTextField
が隠れてしまう場合があるからです。
つまり、入力されている文字が見えないことが発生するわけです。
これをどのように解消するかというと、画面下に適切な空間を空けることで解消されます。
MediaQuery.of(context).viewInsets.bottom
を使います。
これを使うとシステム側で使われるbottomの高さを取得できます。
これが分かればあとはSizedBox
を置いたりpadding
を使ったりで適切に対処できると思います。
画像のようにキーボードを開くだけで、適切な位置にTextField
を持ってくることができました。
画像は、BottomSheet
の下にMediaQuery.of(context).viewInsets.bottom
の高さを持ってくることでできました。
(なのでBottomSheet
の高さが高くなっているように見えます。)
#Rowの両端寄せの罠
Widget
を横に並べるときには、たいていRow
というWidget
が使われます。
このRow
は大変便利です。
- 横の並び方を変えられる
- 横に並べてある高さの違うWidgetに対して、上揃えや中央揃えができる
のふたつの点が好きです。
しかし罠がありました。
それは、Row
でWidgetを両端寄せにする場合です。
皆さんご存じのとおり、こんな感じで両端寄せが実現できます。
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
と使い分けができると思います。
Row(
children: <Widget>[
//左にあるWidget達
Spacer(),
//右にあるWidget達
],
)
こうすることで、もれなく
「左にあるWidget達」は隙間なく左に寄せられ、
「右にあるWidget達」は隙間なく右に寄せられます。
#TextFieldの最大文字数
文字を入力するときは、多くの場合TextFeild
を使うと思います。
その中で何かのタイトルやユーザー名を入力してもらうときは、最大文字数を設定したいと考えると思います。
TextField
にはその機能があるので、実際に実装してみましょう。
TextField(
maxLength:30,
);
こうすると文字数を30文字に制限できます。
ですが、以下の写真のようになります。
人によっては黄色く囲った部分が邪魔だと思う人がいると思います。
私もその一人です。
これを除去するにはmaxLength
を使わない別の方法で文字数を制限します。
以下の通りです。
TextField(
inputFormatters: [
LengthLimitingTextInputFormatter(30), //30文字に制限
],
);
inputFormatters
ではこれだけでなく数字だけの入力にしたりできます。
またList型なのでTextInputFormatter
型を複数オプションとしてつけることができます。
#SVGファイルの表示
FlutterはデフォルトではSVGファイルをサポートしていないので、flutter_svg
というパッケージをインポートします。
dependencies:
flutter_svg: ^0.17.4
※現在(6/18)の最新バージョンは0.18.0です。
表示はこうします。
import 'package:flutter_svg/flutter_svg.dart';
SvgPicture.asset(
'assets/images/image.svg', //画像パス
width: 200,
//ほかにも結構オプションがあります
),
自分はSVGファイルをチュートリアルの画像として使用しました!
flutter_svg
の詳細はこちらから!
#スマホの縦画面固定
自分の作ったアプリは縦画面固定になっていますが、その方法を紹介します。
flutter 縦画面固定
と調べると、よくこれが出てきます。
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp, //縦画面固定にする
]);
これをmain.dart
に書いてもいいんですが、ある時に挙動がおかしくなります。
それはアプリを開くときに横画面だった場合です。
MediaQuery
で画面サイズを取っている場合には、もれなく最悪になります。
他に方法がないかと探しました。
(Androidに限った話だと思いますが、)AndroidManifest.xml
に以下を追記することでアプリを縦画面固定にすることができます。
(activityに書く)
android:screenOrientation="portrait"
横画面の起動時でも問題ないです。
#ファイルの整理
アプリ画面には関わってこないんですが、ディレクトリやファイルの整理をすることがかなり大事だと思っています。
- 開発中に前書いたコードを見に行きたい
- アプリリリース後に修正点が出てきた
このような場合の時どこに目的のものがあるのかが、格段に見つけやすくなるからです。
これは自分が2月ごろに書いていたコードの一部です…
(よく私のPCの中で消されずに生存していたものです)
開発当初、自分は先に言ったことが全く頭になく、コード行が4桁に達するまで一つのファイルに書き込んでしまいました…
ここまでくると修正はかなり面倒です。
(fileに分ける度にimport部分でエラーが山ほど出ます…)
こうなると、ファイルの小分けに全くモチベが出なくなります。笑
新しいアプリ案の開発を始めましたが、そこでは積極的にファイルの小分けを進めています。
頭に入れていることといえば、
- ファイル一つにclassはひとつにする(なるべく)
- class名とファイル名を同等に(class名 : MyBag => file名 : my_bag)
くらいです。
ちなみにclass名やファイル名には命名規則があります。
- class :
UpperCamelCase
=> 先頭大文字・区切り大文字 - ファイル・ディレクトリ :
lowercase_with_underscore
=> 先頭小文字・区切り_ - フィールド :
lowerCamelCase
=> 先頭小文字・区切り大文字
あとはファイルの役割ごとに適切なディレクトリを作って振り分けるだけでファイル管理がかなり楽になると思います。
ぜひ気を付けてください。笑
#アプリ公開のスクリーンショット
iOSはAppStore、AndroidはGooglePlayStoreにデザインの優れた多くのアプリが公開されています。
それに見合うように私もアプリ紹介のデザインには力を入れました。
上の写真のようなスクリーンショットをデザインしました。
右のようなモックアップの素材はこちらからダウンロードしました。
iPhoneやPixelなど主要なデバイスが揃っています。
エミュレーターなどでスクリーンショットをとって、レイヤーで重ねると簡単にデバイスに画面をはめ込んだ写真を作れます。
スクリーンショットでアプリのダウンロード数が変化することもあるので、余裕があればデザインに力をいれてもいいと思います。
#アプリのリリースビルド
(Androidのお話です。)
ここで言えることはただ一つです。
作成したキーはなくさないようにしましょう。
一応ですが、参考はこちらから
https://flutter.dev/docs/deployment/android
https://qiita.com/Renkon117/items/80d9775eb870416d69bf
#大事だと思う話
アプリ開発に限らず開発全体、もっと範囲を広げて、人生で大切なことは何でしょうか?
私が思うにそれは自分で調べてみることだと思います。
調べることで新しい知識や発見を得られるからです。
Flutterは最近YouTubeにも取り上げられたりして、日本でのコミュニティは拡大しています。
ですが日本語の情報は少ないです。
何と比べて少ないかといえば、それは**英語(English)**の情報です。
今回のアプリ開発では、エラーを数えきれないほど吐かれました。
そんなエラーをどうやって解決したか、やはり調べることによってです。
調べるといっても基本的に英語で調べます。
理由はもちろん、日本語の情報が(まだ)少ないからです。
(もちろん日本語の素晴らしい記事もあります。)
日本語で調べたい情報がなくても、英語ならあったという場合がかなり多くありました。
それはエラーに限った話ではなくて、実装したい機能などもあります。
また最新の情報も英語で提供されることがほとんどです。
などなど、英語のドキュメントを読むといいことがたくさんあります。
開発者として調べることは自分でも大事にしていきたいです。
皆さん、林先生はご存じですか?
自分は最近、林先生が出してる本を読みました。
読み出しからけっこうおもしろい。
— thoth000 (@thoth000) June 15, 2020
夜寝る前の軽い読書で読もう。 pic.twitter.com/lgLbYtm6JI
その中でエンジニアに共通するような内容が出てきました。それは、
質問は「意味を考えてすべし」ということです。
皆さんの身の回りには皆さんよりも「スゴい人」がいると思います。
皆さんそんな方に何も調べずに質問したりしていませんか?
質問の価値が小さいような質問ばかりすると、最悪の場合「こんなことも自分でできないのか」と呆れられます。
もちろん「分からなかったらなんでも聞いてね」という方もいると思います。
しかし、少しは自分の力だけで解決できるようになりましょう。
すると「スゴい人」からの評価も上がると思います。
皆さんの周りには「スゴい人」がいるだけでなく、
それを超える「スゴいインターネット」があります。
活用しない手はありません。
#さいごに
ここまで読んでいただいてありがとうございました。
今回が初めて書いた記事だったんですが、楽しんでもらえましたか?
最近(執筆:5月)は、やはりコロナで外出をしにくい状況です。
この期間で、自分の時間の使い方を見直す人も多かったと思います。
こんな時だからこそ、Zikanriでそんな人たちをサポートして、新しい挑戦の手助けをしたいです。
みなさんの毎日の価値をぜひ高めてください。
またこのアプリはアプリ甲子園というコンテストに応募予定です。
アプリ改善のためにレビュー大募集中です!
いい評価も悪い評価も理由をつけてレビューしてくれると大変役立ちます!
コンタクトをとりたい方は、自分のツイッターのDMまでお願いします。
最後までありがとうございました。