大学で創作支援系の研究をしているyasudexiと申します。
この記事では、
初めてFlutterでアプリをApple Store, Android Storeに公開をしたのでそれまでの
- UXで考慮した点
- 実装上のTips
- 公開までの手順
あたりについて公開しようと思います。
デザインのプロセスなども初めてでしたので間違っている部分もあるかもしれません。その場合には優しく切ってくださると助かります。
また、詳しい実装周りに関しては他の方々が素晴らしい知見を公開してくださっているので↓、本記事ではまだあまり触れられていない実装上の細かいTipsにフォーカスして説明します
(追記) と思っていたら、とても参考になった記事2つが削除されていたので、追々書いていければと思います。
UXで考慮した点
まず、アプリ開発中に意識したUXについてです。
UXとは『ユーザー体験』という意味で、UIとよく一緒の文脈で語られますが異なる言葉です。
例えば非常にかっこいい(UIが美しい)サイトであっても、
- ローディング時間が長い
- ユーザーの入力を受け付けないアニメーションが強制される
などの場合、UXが悪いとされます。
個人的な解釈ですが、UXには『煩わしさを感じさせない』(−を消す)と、『機能を体験した際のこれいいなというやつ』(+を増す)の2通りがあると思っており、以下ではケースとして今回の私の思考を書き記しておきます。
UIについては最低限整えてあげる必要はあるかと思いますが、個人的には、機能面に優位性を持たせる個人開発のアプリではUIの優先順位はかなり低いと思います。
UI/UXの参考書・ページ:
UXについて考えた軌跡
というわけで何を考えて作ったのかについて触れます。
まず、今回作ったのはメモスタジオ - 上手い文章を書けるメモ帳というアプリで、いってしまえばただのメモアプリです。
想定するユーザーは自分であり、自身が既存のメモアプリを使っていて困った部分などを解消したもの(UXを向上させたもの)を作りたいと考えました。
これは上述した中ではプラスを際立たせる方なのかなと思います。
- メモアプリは人に送る文章を一旦書いてみる場所として使うことが多いので、上手な文章を書きたい
- 長くなることも多いので一瞬で目次を作って飛べるようにしたい
- iOSのメモアプリは1画面しか開くことができないが、メモを書いてる時にすぐ参照できる『サイドメモ』も書きたい
また、一方でマイナスを感じさせないUXという点では、今のような点でiOSのデフォルトのメモアプリを参考にしました。
- ファーストビューをメモ一覧にする
- 1タップで新規メモを開けるようにする
- ユーザーが最速でたどり着きたいのはおそらく『最新のメモ』か『新規メモ』のはずなので、そこまでの導線の優先順位を一番高くする
上記を気にして作ったファーストビューがこちらです。
プラスの向上の方はアプリのデモ動画をみていただけると幸いです。
「こと」とか「という」みたいな、文章を下手にみせるフレーズを執筆中にハイライトするメモアプリをリリースしました🙌
— やすでぃ (@yasudexi) June 4, 2020
・人に送る用途の文章を一旦メモする人
・メモにすぐに目次を持たせたい人
・メモを書いてる時に『メモのためのメモ』を管理したい人
などに使っていただければ嬉しいです! pic.twitter.com/vNFM1tK5wc
実装上のTips
ここからはFlutterに特有の話になります。
最初に書いておきますが、コア機能の実装の部分に関しては、TextFieldをほぼ再実装みたいなことをしていて自身でもまだUX面で納得していないところがあるので触れません。
MaterialApp
Widgetから順に、大体の構成がわかるように奥に入って説明をしていきます。
なお、状態管理はProvider + Sqfliteを利用しています。
MaterialApp Widget
class App extends StatelessWidget {
const App({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: const [ // 1. localeの設定
DefaultCupertinoLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: const [
Locale('ja', 'JP'),
],
debugShowCheckedModeBanner: false,
// 2. themeの設定
theme: Provider.of<AppPropertyData>(context).buildTheme(),
// darkTheme: ThemeData.dark(),
// 3. routingの設定
initialRoute: '/initial',
onGenerateRoute: router,
);
}
}
1. localeの設定
Android端末ではデフォルトでは日本語のフォントが中華フォントとなります。
これを直すためにはpubspec.yaml
内で以下のように追記し、MaterialApp
内で1.の部分のように指定します。
dependencies:
flutter_localizations:
sdk: flutter
2. themeの設定
続いて、themeの設定をします。
毎回色などのスタイルを指定するのはあまり得策とはいえないので、ここでPrimaryColor
だけでも設定しておくことをお勧めします。
また、 darkTheme: ThemeData.dark()
のように設定しておくことで、『デバイス側ののデフォルトの設定に応じて』変化させることが可能です。
(つまり、『デバイス側の設定を踏襲する』かつ『アプリ内で可変』という状態にするためには別で設定する必要があります)
ThemeData defaultThemeData = ThemeData.light().copyWith(
primaryColor: Colors.cyan[800],
accentColor: Colors.cyan[800],
floatingActionButtonTheme: ~,
appBarTheme: ~,
tabBarTheme: ~,
bottomSheetTheme: ~,
ここではAppBar
やIcon
のデフォルトの色を設定するprimaryColor
や、FloatingActionButton
のテーマを設定しています。
ダークテーマ実装上のTips
また、ダークテーマとの切り替えを行う際の描画に関しては、
『毎回contextからテーマを参照して比較して指定する』というのは実装を見にくくしてしまう結果になりかねません。
ですので、Theme.of(context).brightness == Brightness.light
などで条件分岐を行うのがオススメです。
色の指定について
Primary Colorの指定にはこちらのサイトを見ると想像がつきやすいです。
https://material.io/resources/color/
基本的にはMaterial Colorを指定する方法で問題はないはずですが、もしもう少し別の色も見てみたいという場合には以下のサイトなどが参考になります。
https://www.schemecolor.com/
3. routingの設定
UXのところでも説明しましたが、
「ファーストビューをメモの一覧、そこから一つ戻るとフォルダの一覧」という実装をするため、以下のようにルーティングを設定します。
アプリのルートは/
ですがinitialRoute
で/initial
を設定しているためこの挙動が実装可能です。
また、fluroなどのルーティングのためのライブラリを使うことも良いかと思いますが、個人開発レベルの大きさならばデフォルトで用意されている関数を使って実装するのが良いかと思います。
final RouteFactory router = (settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (context) {
return const FoldersScreen();
});
break;
case '/initial':
return MaterialPageRoute(builder: (context) {
return const Home();
});
break;
default:
return MaterialPageRoute(builder: (context) {
return const Home();
});
break;
Single Source of Truth
なお、これは個人的な意見になりますが、routingを挟む遷移に関しては引数は渡さないように設計した方が良いと思います。
今回はroutingでは引数のやり取りはせず、状態の管理は全てProviderに一任しました。
これはReactにおけるsingle source of truth
という概念に類似しているので、興味があれば調べてみると良いかと思います。
実際に、別のアプリの開発時にはroutingによる引数の受け渡しをしていたこともあったのですが、私の経験上予期せぬエラーにつながりがちでした。
Home.dart
続いて、MaterialAppを開いた後に最初に開くWidgetについてです。
class Home extends StatelessWidget {
const Home({Key key}) : super(key: key);
Future<void> _initData(BuildContext context) async {
// 1. DBからのfetch + 描画するdataのinit
await Provider.of<MemoData>(context, listen: false).fetchMemos();
// (中略)
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _initData(context),
builder: (BuildContext context, AsyncSnapshot snapshot) {
// 2. future builderの結果によって処理を分岐する
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Scaffold(
body: SafeArea(child: Center(child: Text('${snapshot.error}'))),
);
} else {
return Pages();
}
} else {
return const Scaffold(
body: SafeArea(
child: Center(
child: CircularProgressIndicator(),
),
));
}
},
);
}
}
1. DBからのfetch + 描画するdataのinit
アプリの起動時にデータをshared_preferences
およびsqflite
からfetchします。
前者は『初回起動か否か』などを管理するkey-value storeとして、後者は通常のRDBとして使い分けます。
sqfliteについては公式の解説ページを確認しながら実装し、部分的にわからないところを他の記事の内容で埋めていくと理解が早まります。
これによりfetchされたデータを使って諸々の描画をするProvider周りの実装についてはFlutterでproviderを使った画面実装の最小サンプルが参考になるかと思います。
2. future builderの結果によって処理を分岐する
Future Builder
では、データを返す場合と返さない場合で処理が一段階分岐します。
今回はデータを返さない(DBからfetchしてそれをProviderで管理する)ので、
処理が完了したか→エラーがないか
の順番にチェックしています。
データを返す場合は
処理が完了したか→データを持っているか
という風にチェックをします。
(内部のif文でsnapshot.hasData
をチェックします)
また、デフォルトで用意されているものではデザイン的にあまり、という場合にはflutter_spinkitを使うと良いかと思います。
Pages.dart
続いて、Bottom Navigationを用いたページのコントロールためのWidgetです。
class Pages extends StatelessWidget {
const Pages({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final provider = Provider.of<BottomTabData>(context);
return Scaffold(
body: tabScreen[provider.currentTab],
bottomNavigationBar: const BottomNavigation(),
);
}
}
ここについては正直Provider + StatelessWidget
、StatefulWidget
のどちらで実装しても良いかと思います。
わかりやすいと思った方が答えです。
Bottom NavigationのTips
CupertinoApp
なWidgetで作っている場合とMaterialApp
な場合で作る場合、名前と挙動が異なります(ここでは混乱を避けるためどちらもBottom Navigation: BNと呼びます)。
まず前者の場合、画面遷移後もBNが消えません。
それに対して後者の場合、画面遷移によってBNが消えます。
これらは両者のデザインシステムが関係していることなのですが、度々困ることがあるかと思います。
Cupertinoで実装する場合にはflutter/samples - veggieseasons
このサンプルリポジトリのsettings周りの実装が参考になります。
その他のTips
上記の他にも実装の方針上の知りたいことは諸々あるかとは思いますが、長くなるのも宜しくないと思うのでそれぞれ一行でまとめます。
他にも何かあれば一行でまとめたものをコメントでいただければ幸いです。
- Column以下のListViewには
shrinkWrap: true
を入れよう - webページを開くなら
url_launcher
を使おう(webviewをわざわざ使わなくて良い) - OSSライセンスページは
showLicensePage
で自動生成しよう - IconはMaterialIconで困ったら
FontAwesomeIcons
を使おう - ScaffoldのbodyはとりあえずSafeAreaで囲んでおこう
公開までの手順
最後は公開までの手順です。
基本的には公式の手順通りにすれば問題ないのですが、やや面倒なこともあったのでログとして残しておきます。
Build and release an iOS app
Build and release an Android app
アプリのアイコン
アプリのアイコン画像に関してはflutter_launcher_iconsを利用して作成しましょう。元画像のサイズは512x512か1024x1024くらいあれば十分です。
iOS
まずはiOSでのポイントについて。後半の2つは知らなかった単語です。
プライバシーポリシーが必須
iOSアプリではプライバシーポリシーが必須です。後述するApple Store Connectにて入力する必要があります。
私は以下のページから条件に応じて生成→github.ioで静的なページとして公開しています。
この方法であればサーバー代などもかからず履歴についてもしっかりと保管できますので、オススメです。
https://app-privacy-policy-generator.firebaseapp.com/
https://github.com/yasudadesu/policy-pages
Apple Store Connect
自前のiOSのアプリを管理する、Developerのためのサービスです。
アプリ自体はこのサービスに直接アップロードするのではなく、Xcodeでバージョンを指定→flutter build ios
でビルド→Xcodeから Apple Store Connectに送信という手順をとります。
また、個人的にひっかかったのが以下のAppプレビューとスクリーンショット画面です。
- それぞれのディスプレイサイズの解像度・縦横比率に完全にマッチした画像でないとアップロードできない
- 画面を半分くらいにして作業していると
iPad Pro(第二世代)
が画面外に隠れ、審査に出せないことに焦ることになる
あたりはあらかじめ知っておくと良いかと思います。
TestFlight
テスト用にアプリを配布するためのサービスです。
一応審査が必要なはずですが、その審査が通れば多くのデバイスに対して常に最新のソフトを提供できます。
今回は利用していませんが、今度作成するアプリでは使ってみようと考えています。
と、いうのも実はリリースして公開しているのはバージョン1.0.3
でして、1.0.0~1.0.2
はあまりテストができていなかったため公開した瞬間にバグに気付いて停止、ということを繰り返していたからです。
Android
続いてAndroidですが、こちらは特にひっかかるところはなかったです。
アプリのビルド先としてaab
とapk
の2形式のファイルがあるのですが、aab
だけで問題ありません。
両方の留意点
- 連絡先は自身のTwitterアカウントなどでよい模様です。
- アップデート時には直すべき部分が異なります
- iOSの場合、Xcodeを開いてBuild Versionを変更
- Androidの場合、pubspec.yamlから変更
さいごに
初めて型つき言語を体験して、型なしの言語には戻れないなという開発体験をしました(UXがいい!)
ちょっと実装の闇に触れたために本文執筆の際のUXが一部失われてしまっているかもしれないのですが、使っていただければ嬉しいです。
最後に、Flutterを実装していて良かったと思うことを3つにまとめます。
- web,モバイルの開発が初めてでも親しみやすい構文
- 英語でのドキュメントが非常に充実している
- 型付言語のため、予期せぬエラーにあう頻度が非常に少ない
- サンプル実装やパッケージの公開なども盛んで、毎回新しい発見があり楽しい
ここまで読んでいただきありがとうございました。