※2019年05月29日作成の記事です。
はじめまして!
いつも皆さんの面白い記事を読んで、学んだり励みにしたり楽しんだりしてます!ありがとうございます!
このたび東京都八丈島のホテル、リードパーク&リゾート八丈島で伝票システムアプリを作らせてもらいました!
このシステムアプリの全機能はブログの記事で動画を交えて説明しています。
(QiitaじゃTwitter経由でしかアップできないため)
謝辞
ホテルの皆様
今回自分がこんな貴重な機会を得られ、最後まで作ることができたのは、寛容で柔軟なホテル支配人・レストランリーダー・スタッフの皆様のお陰だと本当に思います。
最初は遅延もあったり、レシート2枚出てきたりしてましたが、毎日使用後に多くのフィードバックを得られたので開発がとても捗りました。
「楽しい!」「今までで1番使いやすい」などの声は本当に嬉しいです
Firebase(Google)さん
ありがとうFirebaseとGoogleさん!!!
お陰でサーバーレス・運用費0円で開発できました。
今度 Pixel 3a 買います
作ったもの ---画像---
作ったもの ---動画---
投稿用 注文① pic.twitter.com/diXsgs4DQX
— 山本 燿司 (@baske0806) 2019年5月29日
作ったもの ---図---
- スタッフが客席で注文をタブレットで入力 → DBに記入
- バーカウンターの端末(Fire HD10)がDBの変更を瞬時にリッスン、DBからデータを取って、Bluetooth経由でプリンターに送信 → レシート印刷
というものです。
これをAndroid 上でKotlin & Firebase Realtime Databaseでつくりました。
(技術や機械の選定理由は後ほど)
ちょっと自己紹介
自分は1998年生まれで、神戸大学の経済学部に通っていて現在は休学中です。
もともと文系でプログラミングとは無縁でしたが、半年ほど前に Kotlin のチュートリアル本から学び始めて今に至ります。Kotlin大好き。
次は Flutter & Firebase でなにか作って公開しようとおもいます。
この後は東京に行ってどこかで働きたいと思ってます!
(ご縁があればよろしくお願いします)
このアプリを作ることになった経緯
もともとこのホテルのレストランで住み込みバイトで働いていて、空いた時間に個人開発でアプリを作ろうと思ってました。
ホテルのレストラン: 公式ホームページより
このホテルは最大200名様分の客席があり、レストランは
- 全149席
- 個室宴会場(40席) ある大きいレストランです。
ここで働いているうちに色々な改善点があるなと思うようになってきました。
その内の一つ(かつ最大)が、 ドリンクの注文 → 席に運ぶ までの流れと、伝票計算の処理です。
レストランの業務は開店・閉店作業を除くと主に、以下の作業です。
- お客様を席まで案内 → 最初のドリンク注文をとる
- 各お客様のペースに合わせて食事配膳(和食のコース料理で、約7種類)&空いた皿を下げる
- ドリンクの追加注文を受けるたび、作業中断でドリンクを作り、持っていく。
この①、③のドリンク作業がネックでした。
このように、1人のスタッフが一連の作業を全て行っていました。
夕食の時間帯は3つあって(18時~/18時30分~/19時~)、それぞれの時間帯で約20~50名ほどが一気に来られます。スタッフは1席ごとの注文で1往復するので、最初のドリンクを早く捌かないとその後の全ての作業が遅れてしまいます。
追加注文の際も、作業が中断されるので効率が落ちてしまいます。
そしてお客が全員帰ると、各伝票に各ドリンクの単価を記入して、電卓で合計を計算、記入していました。(多いときは100人分ほど)
この作業をなくしたいと思い、試作品を作って、支配人・ホテルリーダーに見せると即決で「試してみよう」となり、開発が決まりました!
導入後の作業フロー
各スタッフが端末(Fire 7)をポケットに入れ、バーカウンターに通信用の端末1台と、サーマルプリンター1台を置いています。
注文を受けるスタッフ
は、バーカウンターに行く必要はなく、注文入力後、次の席に行ってまた注文を受けられます。
バーカウンターのスタッフ
は次々と印刷されるレシートを見て、ドリンクを作り、お盆にレシートと一緒にドリンクを置いていきます。
そして手の空いているスタッフ
が、そのお盆を持ち、レシートに記されている席まで運びます。(レシートはどんどん重ねて挟んでいく。)
このように、作業の分業化ができるようになりました!!
さらに単価もレシートに記載されていおり、最後に1枚1枚伝票を計算する必要もなくなりました!
使用した技術・機器の選定理由
ハード面
Why Fire7 ?
自分がここのホテルに来る前、他の伝票サービスを導入する試みがあったらしく、もともとFire 7 はありました。(結局導入には至らなかったそうです。)
それに、ホテルのソムリエエプロンのポッケに丁度フィットします
Why(サーマルプリンター) StarPrnt TSP650||?
スター精密は安心できるし、SDKもしっかりしていて、尚且つ安価なのが決めてでした。(4万円弱)
このモデルはBluetooth通信に対応していて、印刷速度も1番早かったのでこれに決めました。
ソフト面
Why Android?
もともとFire端末があり、自分もAndridを学び始めたばっかだったから。
(Fire OSは、独自といってますがほぼAndroid。)
この偶然はラッキーでした。
Why Kotlin?
自分が学び始めたのがKotlinからだったから。
(Javaと100%相互互換なので結果的にJavaも段々わかってきた。)
Why Firebase?
最初はAWSかAzureと思っていたのですが、Firebaseは無料枠(しかも大容量)があり、それにDBの性能が最高でこれだけで十分だったからです。(とくにリアルタイム同期とオフライン処理)
Firebaseの無料枠が、AWS(Amazon)・Azure(Microsoft)との競争の中で勝つための策だとしたらAWS, Azureにも感謝です。
👇100名ほどのディナー1日で、この容量なので、まだまだまだ余裕があります。
Why Firebase RealtimeDB?
これは、FireStoreと悩んだのですが結局RealtimeDBにしました。理由は、
- Realtime DBは Key-Valueなので、高速。
- FireStoreの特徴の強力なクエリは必要なかった。
ベータ版なのでちょっと気が引ける。
でしたが、よく考えると お客130人前後の注文情報なんて500Bぐらいで済むので、遅延とかはそこまで変わらないと思います。
それに最近知ったのですが、FireStoreの方はGoogleのインフラをフル活用していて、今後GoogleはFirestore推しでいくらしいです。
ですが、β版というのが気になりました。
次の個人開発からはFirestoreを使います。
※追記
@chronicle さんのご指摘で、見落としに気付きました。ありがとうございます!
Firestoreは現在正規版で、GCPのサービス品質保証契約(SLA)も適用されてます! どんどん使いましょう!
課題解決
ここからは、開発の過程でつまづいた所、その解決策を書いていきます。
レシート1枚で全ての情報を表す。
分業化するにあたって、注文を受ける人
・ドリンクを作る人
・運ぶ人
はレシート1枚を通して情報伝達を行う必要があります。必要な情報は、
ドリンクを作るのに
- ドリンク名(もしくは商品名)
- その飲み方(お湯割りやロックなど)
- あればオプション(「常温で」、「レモンつけて」など)
テーブルまで運ぶのに
- テーブル番号
会計用に
- そのテーブルの全注文履歴と合計金額
- 各ドリンク(商品)の単価
- お部屋番号(会計は部屋付けなので)
以上の情報を1枚のレシートに入れなければなりません。
最初はStarPrntのSDKを使えば、なんか良い感じに自動で割り振ってくれると思ってました(希望的観測)。
が、実際は自分でテンプレートを作ったり、行数・フォントサイズを調整する必要がありました。
これはこれでめっちゃ楽しかったです。まず普段のレシートに目が行くようになって、「あ~これ手抜いてるな」とか「すげぇ!どうやって!?」みたいに思うところが増えました。
コードを貼るとめちゃ長くなるので、別記事でレシートの作り方はまとめます。
※追記※書きました!
【誰得】普段目にする、レシート印刷の実装方法
少し言うと、縦横を揃えるためにすべての文字を全角にして、計算して空白を適切な数入れたり、1行に収められるようにしたりしました。👇
(半角のサイズは全角の半分でないため、混ざっているとややこしい。)
var order = Transliterator.getInstance("Halfwidth-Fullwidth").transliterate(orderList[x])
if (order.count() >= 18){
order = order.substring(0, 18)
}
val size = order.count()
val rest = 22 - size
data = (order + " ".repeat(rest - 3) + qList[x] + "\n")
以下の点に気を付けました。
- お客様にとって重要な情報は大きく太く(ここはお客様の年齢層も高く、見やすいレイアウトを心がけた)
- お客にとって重要でない情報(日時、作り方、端末番号)は、小さく明記
- 注文ごとをブロックで分け、どの注文を作れば良いのか、わかるようにした。(常に1番下のブロックのドリンクを作る)
これで、お客
は何を頼んで合計は何円かわかるし、ドリンクを作る人
も何をどのように作るのか、運ぶ人
もどこに運ぶのかを一目でわかるようになりました!
注文ごとをブロックで分けるにはDBの設計を見直す必要がありました。
DB設計で気をつけたことは後述しますが、これは柔軟なNoSQLモデルだからこそ簡単にできたのだと思います。
プリンターの排他制御
これはめちゃくちゃ悩みました。まず、排他制御自体を知らなかったので探そうにもGoogleは教えてくれず、、、だったのですがMENTAというサービスでメンターの方に相談するとすぐに解決しました。
Kotlin(Java)ではとっても簡単で、
@Syncronized
fun a (){
//処理
}
👆このように関数の上に @Syncronized
を入力するだけで排他制御が実装できます。
あとはコードの設計を見直して、排他制御を実装した関数内で
DBからデータの読み取り → レシート情報作り → 印刷 → 初期化
の処理を行うようにします。
13台同時に注文してもきちんと印刷されました
DBの設計
Firebase RealtimeDBでは、リレイショナルなモデルではなく、データをJSONツリー型で保持するNoSQLモデルです。
自分はリレイショナルなDBをほとんど触ったことがなかったので、逆に変な違和感とかはなかったです。
このモデルで大事なことは2つで、
- 余計なデータまで読み込まないように、深くネストしない
- 効率よく必要なデータを見つけるための、平坦化(非正規化)
です。さらにこのアプリでは、DB設計を考えるにあたって
- 各注文ごとをブロックで分ける(理由は上記)
- 簡単にレシート印刷できるようにする
という事を考えながら設計し、以下のようにしました👇!
(11番テーブルの注文のみ)
RealtimeDBでは、最大34回ネストできるのですが、ここでは5回に抑えることができました。
そしてどのデータもテーブル番号 (Table Number) (画像では TN 11)と紐づけることで、注文2回目以降は、テーブル番号入力後、すぐにメニュー画面へ遷移でき、途中でデータの削除や数量変更などもできます。
また、一番上の**"Checker"ノード**を作ることで、、効率的にレシートを印刷できます。
色々なクラスからレシートを印刷する必要があるのですが(注文履歴変更後や、確認用など)、それも以下のコードで実装できます。👇
val table = "11" //実際のコードでは、この値をintentで渡している。
//実際には、トランザクション処理
val mDatabase = Firebase.getInstance().getReference("Checker")
mDatabase.child("TN $table").child("check").setValue(false)
mDatabase.child("TN $table").child("check").setValue(true)
それに、プリンター横の端末は、"Checker"ノードにリスナーを設置し、"check"の値がfalse → trueになるのを監視するだけでいいので、こうしています👇
val check: Boolean = p0.child("check").value.toString().toBoolean()
val tablet = p0.child("tablet").value.toString()
if (!oldValue && check){ //oldValueはリスナー外で定義している
val tableNum = p0.key.toString()
readData(tableNum, tablet) //テーブルの情報を全部読み取り、レシート印刷
}
oldValue = check
時々レシートが2枚印刷される問題は、この実装で解決しました。
直感的なUI
前に導入しようとしていた伝票システムは、全体的に単色で、メニューの選択画面が文字のリストで、とても見にくかったとのことでした。
なので自分は極力文字を少なく・画像やベクター図を多く、見やすい色やフォントサイズを心がけました。
また、1つの画面で全ての操作を終えるのはなく、各画面で行う操作は1つにしました。
スタッフは画面毎の単純な質問に答えるように操作していきます。
(例えば、日本酒を選択するとおちょこの数
を質問し、ワインだとグラスの数
、焼酎だと飲み方
、ソフトドリンクだと、氷の有無
やアイスorホット
を質問します。)
流れるように素早い操作が可能なように心がけましたが、そうすると一方でミスが増える可能性がでてきます。
なので、できるだけミスを無くすために、また、お客様の急な変更に対応できるように(「やっぱビール3つで!」みたいな)、
注文が3種類以上の時は自動的に【注文内容は大丈夫?】
画面に遷移するようにしました!
ここの画面から、商品の追加や数量変更・削除も可能です。
Qiita投稿用 pic.twitter.com/Dx0tktsMii
— 山本 燿司 (@baske0806) 2019年5月29日
コツコツ改善 ①SoftKeyboardのフォーカス調節
あとは、小さなことなんですが、注文の最後の画面では、EditTextのフォーカスをデフォルトで外しているのですが、メモを残す
ボタンタップ時にソフトキーボードがでてくるようにしました!
このコード👇で、ソフトキーボードを出す実装ができます。
fun showSoftKeyboard(view:View) {
if (view.requestFocus())
{
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
}
}
:参考ページ 【How to Hide and Show Soft Keyboard in Android】
コツコツ改善 ②ホーム画面整理
これは、システム関係ないのですが、もし誤操作でホーム画面に戻ってしまっても、一目でどれが伝票システムアプリかわかるようにしました。(完全人力)
終わりに
ハード機器の選定から、DB設計、UIまで、全部自分1人でやると色々わかってきますが、1番思うことは、自分のレベルの低さです。(本当に)
そして、フィードバックの大切さ。個人開発のアプリでは今回のような直接的なフィードバックがない分、色々なLogや、ユーザーのデータが相当すると思うので、次に活かしたいです。
あとはメンターの大切さ。今回自分は登録だけして使いませんでしたが、英語版ではCodeMentor、日本語版ではMENTAなどがあるので、積極的に利用していきたいです。
最後まで読んで頂いてとても嬉しいです!
ありがとうございました!