ベータコンピューティングの活動や技術、開発のこだわりなどを紹介するブログです。



【IT×他業種インタビュー】第2回:建設業 有限会社岡本アルミガラス 代表 吉野茂樹様

こんにちは。Beta Computing株式会社の吉村です。
今回は、皆様がどのような業務をこなし、どういった課題に直面し、どんな対策や取り組みを行っているのか直接お伺いして取材させていただく企画の第2回目です!

IT×建設業インタビュー

第2回目として取材させていただきましたのは、窓や玄関に関する工事・販売を津幡町・かほく市で提供されている 有限会社岡本アルミガラス 代表の吉野茂樹さんです。
よろしくお願いします!



商圏を津幡・かほくに絞り、高品質のサービスと充実したアフターサポートを提供する

──事業内容を教えて下さい

吉野さん:
メイン事業は、家の玄関・勝手口・窓など「開口部」となる箇所の販売(大工、工務店など)や工事を手掛けています。
低価格で高品質、窓ガラス交換早急に対応、地元業者ならではの柔軟な対応力、という強みを活かしてサービスを提供しています。

──地元ならではの強みとは具体的にどういったものですか?

吉野さん:
現在は広範囲でサービスを販売するお店も多くあります。
商圏は広がりますが、その反面アフターサポートがやりづらく、すぐに動くことが難しくなります。
岡本アルミガラスは商圏を津幡・かほくに絞ってサービスを提供しています。
ガラスが割れた場合や戸締まり周りの工事・修理は急ぎで必要とされることが多いので、すぐに動けるような体制を維持しています。

昔から繋がりのある会社さんや地域住民、チラシやネットで知ったお客様や口コミで知った方々などからご依頼をいただいております。

──創業から50年以上経っていますが、当初から地元を中心に?

吉野さん:
創業当初は大工さんがお客様で、サッシを販売していました。
大工さん、工務店などの卸しをもともとメイン事業でやっており、それが徐々に一般消費者である個人のお客様と直接つながるようになってきました。
現在では、個人と企業5:5の売上金額になっています。
件数で言えば、個人の方が多いです。

──現在はどれくらいのご依頼があるのでしょうか?

吉野さん:
ガラスの交換や鍵の交換など、足が速く単価が安いものも含めると年間顧客は元請け下請け含めて700件ほどありますが、弊社は従業員2名と現場2名、事務員のメンバーで対応しています。

──年間700件以上の件数をなぜ少人数で対応可能なのでしょうか?

吉野さん:
地元のお客様がほとんどのため移動時間が短く、1日に数件対応することができます。
作業時間も短いもので数時間、長いものでも1日で終わります。

地元を中心に展開することで、少人数の体制でも年間700件の依頼を受けることができます。
商圏を広げることを考えることもあるのですが、現在のサービスとアフターサポートの品質維持が難しくなります。
また、大手や中小のライバル店が多くなり、値段ばかり見られてしまいます。
営業範囲も拡大する必要があり広告費もかかるので、商圏拡大にあまりメリットが感じられません。

小さな依頼でも丁寧に対応する。大手にはできない

──現場の基本的な業務の流れを教えて下さい

吉野さん:
例えば玄関窓の取替えだと、
①お客様からご依頼を受注し見積もりを提出します
②見積り金額をご検討いただき、ご注文を受けます
③現場を確認し、対象物のサイズを測ります
④メーカーへの商材の手配が必要であれば発注します
⑤入荷日確認後、お客様のご都合を確認し、工事日を決定します
⑥工事完了後、お客様へ請求書を提出します
⑦入金確認
といった流れです。
基本的に1日で工事が終わることがほとんどになります。

他の大手では営業ノルマがあり、基本的に大きな数を売りたいと思っています。
そのため、窓1箇所の対応などは引き受けて貰えないこともあります。
ですが、お客様にとっては1箇所も数箇所もあまり関係ありません。
私たちはむしろ、まずは1箇所試してほしいと思っています。
そして、さらなる注文や、何かあったときにまたご依頼していただければ良いと思っています。

様々な家があり、家によって作りが違うため対応が難しいものもありますが、年間700件以上の対応をやってきて、家によって最適な対応が感覚でわかるようになってきました。
1箇所の対応でもご依頼いただければと思っています。

──業務の中で課題はありますか?

吉野さん:
見積もりや経理などの事務処理が大変です。
人件費がかからないように全て1人で対応しています。

──ITを活用されていますか?

吉野さん:
メーカーのシステムで、請求書や領収書を書式で出すものを利用しています。
YKK APのシステムで、YKK APが展開するMADOショップ事業の一環です。
これまではPCで請求書などの書類を作成していましたが、顧客管理はできていませんでした。
システムを入れてからはどのようなものが売れているか、粗利や各項目の割合、仕入れに関する情報など、細かく管理できるようになりました。
年間顧客件数など見れるので、顧客解析も可能です。

また、MADOショップの運営サイトに津幡中須加店としてホームページを公開しています。
ホームページ、書類、顧客管理などすべてYKK APのシステムを利用して作業することができます。
MADOショップとしての費用も少しかかりますが、DMの発行もできますし、大きなメリットを感じています。

──御社にお伺いしたときMADOショップのことが気になっていました

吉野さん:
一般消費者向けに販売するための窓口がMADOショップです。
MADOショップは「ニッポンの窓をよくしたい」という理念のもと、YKK APとパートナーシップを結び、全国で活動する窓リフォームのお店です。
YKK APから「MADOショップのお店をしませんか?」と声をかけていただいて参画しています。



最適な宣伝方法を検討しているが、答えは出ていない

──業務の中でITを活用されているものはありますか?

吉野さん:
業務連絡やお客様とのやりとりなどコミュニケーションは電話がメインですが、人によってはLINEを利用する方もいます。

──IT活用に興味や関心はございますか?

吉野さん:
ITは重要視していかないといけないと思っています。
現在は年配のお客様が多いので、年に4回くらいほど折込チラシを出しています。
ただ、新聞を取らない人も増えてきていますので、今後チラシの効果が減っていくでしょう。
広告宣伝費の使い方や方向性を改めて検討する必要性があると思っています。

ただ、一般消費者に向けての最適な宣伝方法は現時点ではわかっていません。
MADOショップのページでも工事・修理事例に費用を記載していないお店がありますが、消費者の方の参考にしてもらえるように、弊社は金額を載せています。

──お客様の年代や性別などターゲット層はどのような種類ですか?

吉野さん:
家を立てて20年以上経つ一般の方がメインターゲットです。
おおよそ20年経過頃から玄関や窓に不都合が出始めます。
仮に30代で家建てたとしたら、50〜60代の方々です。
ただ、中古住宅を購入した方もいらっしゃるので、その限りではありません。

また、外壁は10年くらいで寿命がくるものがあるので、そういった周期の方々に目に留まるよう広告を打つようにしています。

──同業者とのつながりはありますか?

吉野さん:
津幡町商工会を中心に、地元の大工さん、瓦屋さん、水道管屋さんなどとのつながりがあります。
例えば住宅のお仕事では、1つの業者様が請負し、得意分野のお仕事をそれぞれ担当するよう紹介したりします。

サッシの同業者でいうと、ガラス組合というものがあり、MADOショップというくくりでも県内でも9社ほどつながりがあります。
親子で経営していたり家族でやっている会社が多く、繁忙期などは近隣のサッシ屋さんに応援依頼することもあります。
横の連携は取れていると感じています。

昔から連携はありますが、最近は若手世代が横のつながりを大事にしています。
逆に、親世代の方が周りをライバル視しているイメージです。

また、残念ながら廃業も増えています。
跡継ぎがおらず、親世代でやめる人たちも多いです。
現在では、津幡やかほく市周辺だと3~4社になると思います。

──コロナや戦争の影響はありますか?

吉野さん:
コロナ渦で、お客さんの購買意欲が下がりました。
また、家に直接お伺いすることができなくなってしまったので、工事キャンセルが相次ぎました。
そのため、売上が1000万円ほど落ちることもありました。

昨年は換気需要で網戸などの受注が増えていたのですが、戦争の影響によりまた購買意欲が下がってしまいました。
現状、チラシを出してもこれまでより反響が薄いです。
以前よりお客様の財布の紐が硬いと感じます。

──建築の建材などの材料費が高騰していると聞きます

吉野さん:
原価がすごく上がりました。ガラスは燃料高騰で戦争以前より2〜4割値段が上がっています。
具体的には、6000円の原価ガラスが9000円になりました。

──費用も変わっている?

吉野さん:
やはり見積り作成時点で、これまでより2割~4割価格が上がっています。
原価の高騰により、定価表記も上がっています。
そのため、お客さんの負担が増えていくのが心苦しいですが、仕方がない部分もあります。

──最後に、読者の方へのメッセージをお願いします

吉野さん:
弊社の職人はプロフェッショナルです。
直ぐに、丁寧に、迅速にご対応することを心がけています。

お客様の満足を第1に考えております。
良い職人さんがきれいに仕上げることで、お客様に喜んでもらえることがやりがいです。
事務、経理、現場の職人含め全員で努力しております。

玄関・窓でお困りの際はぜひお声がけください。


──以上になります。ありがとうございました。

吉野さん:
ありがとうございました。

有限会社岡本アルミガラス様 連絡先

〒929-0332 石川県河北郡津幡町中須加ろ27-2
有限会社岡本アルミガラス
代表 吉野 茂樹
http://okamotoarumi.com/

日本標準産業分類

大分類:建設業
中分類:職別工事業
小分類:ガラス工事業、金属製建具工事業



おわりに

今回は、津幡町にある岡本アルミガラス様に取材させていただきました。
実は、弊社メンバーの1人が岡本アルミガラス様に2重窓の取り付けを依頼したことがきっかけで取材に至りました。

窓の取り付けを数社に見積りしたところ、岡本アルミガラス様の価格が1番納得できたそうです。
また、取り付け場所の確認において、作業や説明が丁寧だったことが依頼の決め手に。
実際の取り付けもきれいで手際良くすぐに終わったようで、大変満足しているとのことです。
取材を通して、地元のお客様を第1に考えていることをとても実感することができました。

吉野さん、この度はお忙しいところインタビューを快くお引き受け下さり、本当にありがとうございました!

本内容が少しでも皆様のご参考になりましたら幸いです。
今後も他業種インタビューを更新していければと思いますので、次回をお楽しみに!

【IT×他業種インタビュー】第1回:運輸業 野々市運輸機工株式会社 代表取締役 吉田章様

こんにちは。Beta Computing株式会社の吉村です。

ITの活用がどの業界でも叫ばれるようになってから数年が経ちました。
しかし、現場レベルではまだまだ浸透しているとは言えず、活用しきれていないと感じることが多くあります。
その度に弊社は皆様にとってどのような存在でいればよいのか、兼ねてより、より具体的に価値を見出そうと考えておりました。

どのような価値を提供できるのか──。
どのような存在になれるのか──。


皆様が、どのような業務をこなし、どういった課題に直面し、どんな対策や取り組みを行っているのか、まずは知らなければならない。
であれば直接話を伺う方が正確で早いと、インタビューする企画を思いつきました。

IT×運輸業インタビュー

第1回目として取材させていただきましたのは、運送事業をメインに手掛ける 野々市運輸機工株式会社 の代表取締役である吉田章さんです。
よろしくお願いします!



大手がやりたがらないものをやる。そこにニーズがある。

──事業内容を教えて下さい。

吉田さん:
お客様にあった物流を提案させていただき、お客様にあった運送を行っています。 主に、北陸3県を範囲とした運送と北陸-大都市圏の運送がメインです。 様々な運び方がありますが、主力の1つに重たくて長いモノに特化した運送サービス"メタル便"があります。
全国に構成された運送ネットワークを使い、北は北海道から南は九州までリレーのバトンを渡すように目的地まで荷物をお届けしております。
メタル便は全国を網羅できるよう中小企業7社で作っています。

──7社はどうやって?

吉田さん:
メタル便の歴史は古く、2000年から開始されているサービスになります。
メタル便は長尺物や重量物を運送するサービスですが、この長尺物や重量物というのは大手運送会社が嫌がる荷物で、業界では「ゲテモノ」呼ばわりされていました。
大手が運送の対象物としなかったことで、それらを運ぶ需要が生まれました。
関東では多くの重量物が運ばれ、大阪では長尺物ばかり集まっていたようです。 「総合トラック」という会社が中心となり、関東と大阪をサービス圏にメタル便が開始され、それが2000年になります。
弊社は2016年度に北陸地域の運送会社として加入し、現在では7社となっています。


──メタル便をはじめてみて反響はありましたか?

吉田さん:
メタル便が利用できるからこそ商売を続けられる会社さんもいらっしゃいます。
メタル便として北陸地域に向けてDMをうった次の日の朝一番から「こんなサービスがあるのか!」とDMを握りしめたお客様が訪ねてきました。
「メタル便がなければ商売をやめていた」と言われたこともあり、お客様から求められていることを強く実感できました。

現在、働き方改革や2024年問題によりドライバーが不足しており、今後も需要が高まると予想されます。
さらにはガソリン高騰によって大手企業の費用も高くなっているので、メタル便に注目が集まっています。

──御社の強みをお聞かせください。

吉田さん:
北陸エリアでの混載・共同配送を得意としております。
また、メタル便では大手が引き受けない長尺物・重量物の全国運送を行っています。
その他では、大都市圏の大手企業が北陸まで荷物を運ぶのは手配の手間やコストがかかるということで、北陸に拠点(ヤード・倉庫)を作りたいという企業が増えてきました。
これまでは、高速道路の整備やトラックの普及が進み、地方の在庫が無くなっていき、東京や大阪に集約される動きでした。
ところが、長距離ドライバーの労働時間規制の動きが大きくなり、大手企業はコンプライアンス違反を気にするようになったため、地方に在庫を確保し工期に合わせて運送するような形に変わっていきました。それにより、保管業務の依頼が増えてきています。
弊社は機械等を運ぶだけでなく、お客様の工場内へ搬入し据え付けまで行います。
大型機械等の移動も専門に行っており、お客様の工場内にある機械のレイアウト変更などすることも可能です。


──お客様の業種は?

吉田さん:
製造業、建築業など、大きくて重たいものを運ぶ必要がある業種です。
機械装置や、鋼材・鉄・ステンレスの素材、木材など建築資材を運送します。

──現場の業務の流れを教えて下さい。

吉田さん:
現在40人ほどのドライバーさんがいます。ドライバーは長距離ドライバーと地場ドライバーにわかれます。
長距離ドライバーは北陸3県から大都市圏にモノを持って行く、大都市圏から北陸にモノを持って帰ってきます。
地場ドライバーは北陸3県内にモノを持って行きます。毎日日帰りになります。
長距離ドライバーは泊まりになることが多いです。1日かけて運送します。

──長距離ドライバーは県を跨ぐことになります。コロナの影響はありましたか?

吉田さん:
県を跨ぐことによる影響ですが、ドライバーは1人の時間が長いので、感染者もなく、県外には行くが他の人と触れ合う機会は少ないので感染者が出た等は特にありませんでした。
ただ、お客様の出荷が止まってしまったため、売上に大きく影響がありました。

──業務の中で課題はありますか?

吉田さん:
課題はたくさんあります。
外部環境としてコロナが落ち着きそうで落ち着かないことと、さらにはウクライナでの戦争も始まってしまいました。
原油高が非常に辛く、同じ距離(少ないくらいなのに)で昨年度より1千万円のコスト差が出ております。
どうしても外部環境の影響を受けてしまいますね。

社内では、直接のコミュニケーションを取りづらくなってしまいました。
飲み会や食事でのコミュニケーションが減り、細かな情報共有や社員の調子などわかりづらくなっています。
チームワークが重要であるのに、それを強める機会が減っています。

IT導入はスモールスタートが良い。受け入れてもらうことが大事。

──情報共有について、ITツールを使用していますか?

吉田さん:
ITや情報に興味があり、10年ほど前から会社の基幹システムを入れ替えました。
また、社員とのコミュニケーションでいうと、2018年に社内でスマートフォンを支給しました。
スマートフォンの普及が進み、父親60代でも使えているのを身近で感じたこともあって、全社員に渡しました。
一番の目的はチャットツールを利用するためです。

支給前の業務連絡は電話でを行っており、例えば、どこにモノを降ろしました、卸し終わりました、どこに向かっていますなど、社員みんなが電話を掛けるため、つながらないことがしばしばありました。
以前から電話での情報共有に不満があったので、業務連絡は基本電話禁止し、全てグループチャットでやることにしました。
つながらない、折返し、などの作業の無駄が無くなり、また、エビデンスとして残るようになったため、言った言わない問題も無くなりました。
注意事項の情報共有など、すぐに周知できるのも良いところです。

──ちなみに支給したスマホはiPhone?Android?

吉田さん:
通信会社から良い提案があったので、Android端末を支給しました。
格安スマホなどもまだ無かった中、ショップでは無理でしたが、通信会社と直接交渉したところ、安く提案いただけました。

──スマホを支給したことによる社員の反響はありましたか?

吉田さん:
60代社員でも使えており、業務中の電話がすごく減りました。
1人だけ64歳の人が3日たっても電源の入れ方がわからないと戸惑っていましたが、すぐに解決しました。
1週間で電話からチャットワークの体制に変更できたのは良かったです。

また、情報のやりとりがグループチャットになので、誰がどこで何をしているのかの会話が見えるようになりました。 当事者同士の会話が見えなかった以前は、あいつばっかり楽をしているではないか等の不満が募っていたのが、グループチャットだと他の人の苦労が見え、他人への不満が減りました。

──チャットワークを選んだ理由を教えて下さい。

吉田さん:
みんなが使いやすいシンプルなのはチャットワークかなと思い選びました。
使用に対する社員からの声は特にありません。ただ、何もないことは良いことだと思っています。

チャットワークにはAPIの提供があるので、それを利用していけばいろいろな情報を自動的に社員に発信できるようになります。 自分で調べながら工夫して進めています。
例えば、ありがとうカードをチャットワーク上で行えるようにしました。
ありがとうカードとは、他人への感謝を伝える取り組みになります。
ドライバーは孤独な作業が大半であり、会社で褒められることが多くありませんので、社員同士で感謝の言葉を紙に書いて社内に設置したポストに入れるようにしていました。 今では会社のカルチャーとなっています。
ただ、これまでに何千枚と紙が使われていた上、外出中のドライバーでも利用しやすいように、ありがとうカードをチャットワークに移行しました。
Googleフォームとスプレッドシートとチャットワークを連携し、自動で配信・集計するようにしています。
加えて、くじびき機能もつけて100分の1で当たりが出るという、少し遊び要素も入れています。

──社内・社員のITに対するイメージは変わりましたか?

吉田さん:
根本は変わっているわけではないですが、慣れていっていると感じます。
ITを勉強しているわけでは無いので、プログラミングできる人が増えましたとか、どんどんITツールを提案してくる人が増えたということではありませんが、ITツールを使いましょうということにたいしては受けいれてくれます。
コミュニケーションインフラとしてチャットワークもみんな利用してくれています。

現在、これまで紙で管理していたものがどんどん電子化していってます。
FAXをpdfで保存し、サーバーからGoogleドライブに自動保存し、自動でチャットワークに配信するようにしました。
FAXもできるだけ出力しないようにしています。
ただ、お客様側がFAXなので、現状は完全に無くすのは難しいです。

──業界ではITやIoTの活用は進んでいない?

吉田さん:
同業者はまだIT化が進んでいないと思います。
IT導入は、スモールスタートで費用をかけずに進めていって、ここから先は自分たちでは無理だと感じたときに本業のソフトウェア会社に依頼できるような流れが良いと思います。
自分たちで少しでもITを理解、挑戦していくことが必要だと思っています。
弊社としても同業者にITを普及させていきたいと考えています。

今後も積極的にITを活用していく。AI分野にも興味がある。

──新しいIT活用に興味・関心はありますか?

吉田さん:
興味がありすぎて・・・

Withコロナは今後も続いていくと思っています。
人が集まれる機会や時間が少なくなって、本来はもっと集まって社員教育やコミュニケーションを取れればと思っています。
現状は、社員教育をスマートフォンで見る動画のサービスを利用しています。
年に3回くらいの集合型もあったりしますが、まだまだ少ないと感じます。
最近ではそれが難しい時代と考えて進めていった方が良いと捉えるようになりました。
さらにITツールを活用し、社員教育を改善できればと思います。

また、現在は運送業とIT業のコンソーシアムに参加しています。
1日に百枚~千枚という数のFAXが届くので、紙から電子化をAIを使って自動化できないか実験を行っています。
FAXから自動で情報を取得というのは、AIが進化したからといって自由記述が多いとやはり難しいとわかってきました。
ですので、現時点ではソーター(仕分け作業)だけでも自動化できないかと考えています。

──今後、連携したい他業種はありますか?

吉田さん:
やはりIT企業ですね。
IT化進み、ITが現実空間を飲み込んでいると感じています。

様々なIT企業が物流業界に参入してきてますが、まだまだ攻略できてないのが現状だと思います。
どこもイニシアチブをとれたと言えるところまでは行っていません。

物流の全部は難しくても、セグメントでわけることで運送でシステム化をめざしていくといいのではと思っています。
メタル便はそれの一つになりうるかもしれません。

──以上になります。ありがとうございました。

吉田さん:
ありがとうございました。

野々市運輸機工株式会社様 連絡先

〒920-0211 石川県金沢市湊1丁目55番地23
野々市運輸機工株式会社
代表取締役 吉田 章
https://nonoichiunyu.com/

日本標準産業分類

大分類:運輸業
中分類:道路貨物運送業
小分類:一般貨物自動車運送業


おわりに

普段聞けない大変貴重なお話をお聞きすることができました。
業界の動向や、ITの取り組み方、ITに対する捉え方など、勉強になることが大変多くありました。
60代の社員がいらっしゃる中で、社内のスマートフォン導入がスムーズに進んだことは驚きでした。

吉田さん、お忙しいところインタビューを快くお引き受け下さり、本当にありがとうございました!

本内容が少しでも皆様のご参考になりましたら幸いです。
今後も他業種インタビューを更新していければと思いますので、次回をお楽しみに!

Jetpack Composeに入門してみた

弊社Android・クロスプラットフォーム担当のd-ariakeです。
先日 Jetpack Compose の正式版がリリースされたので、さっそくうちでも試してみることにしました。

作ったもの

以前の記事 でご紹介したQiitaの簡易クライアントアプリと同じものを今度はJetpack Composeで実装してみました。
リポジトリはこちらです。
https://github.com/BetaComputing/SimpleQiitaClientAndroid/tree/compose

イメージ

良いと思った点

Jetpack Composeを使ってみて、4点ほど良いと思った点があったのでご紹介いたします。

角丸などの細かなUIデザインが簡単

このアプリでは ユーザアイコン画像記事タグ に角丸を設定しています。
従来のViewでは、角丸を設定するのにbackground用のdrawableを作らないといけませんでした。
(参考: AndroidでViewを角丸にする - Qiita)

しかし、Jetpack ComposeではModifierで

RoundedCornerShape(corner = CornerSize(4.dp))

をセットすることで、簡単に角丸を実現することができます。
(該当コード: アイコン画像, 記事タグ)

また、ripple effect を付けたい場合も、

indication = rememberRipple()

をセットすることで、簡単に実現することができます。
(該当コード: 記事項目, 記事タグ)

このように、細かなUIの作り込みはJetpack Composeの方がラクに実現できるかと思います。

リストUIが簡単

従来のViewでは RecyclerView を使ってリスト系のUIを実装していたと思います。
RecyclerViewではAdapter/ViewHolderの実装や、リストの変更通知の仕組みが大掛かりになりがちで大変だったと思います。

Jetpack Composeでは、描画の仕組みが従来から大きく変更されているので、RecyclerViewのようなViewを使いまわしたり、細かな変更通知を行う仕組みが不要になりました。

LazyColumn を使って、

LazyColumn(
    modifier = modifier.scrollable(state = rememberScrollState(), orientation = Orientation.Vertical),
) {
    items(items = articles ?: emptyList(), key = { article -> article.id }) { article ->
        ArticleView(
            article,
            onArticleClicked = viewModel::onArticleClicked,
            onTagClicked = viewModel::onTagClicked,
        )
    }
}

このようにスッキリ書けるようになりました。
(該当コード)

ただし、まだスクロールバーまわりの機能が弱いみたいなので、今後の進化に期待です。🚀

MVVMパターンの考え方がJetpack Composeでも通用

このアプリでは、UI以外の部分 (ViewModelから上位のレイヤ) に対してほとんど手を加えずにJetpack Compose化することができました。
既存のコードがMVVMパターンで設計されている場合、ViewModelから決まってくる状態の、UIへの紐付けの記述の仕方が少し変わるくらいで、従来とほとんど変わらない設計になると思います。
既存の知識があれば、ほとんど学習コストを掛けずにに対応できると思われます。

参考: 状態と Jetpack Compose | Android Developers

既存のアプリにも部分的に組み込み可能

Jetpack Composeには ComposeView という従来のViewにJetpack ComposeによるUIを組み込むための仕組みが用意されています。

このアプリでも ↓ のコミットで、RecyclerViewのみを部分的にJetpack Compose化し、検索用UIの部分などは従来のViewのままで実現するといったことをしてみました。

記事リスト部分をJetpack Composeを使って実装 · @9e4c88d

xml中での RecyclerView 使用箇所を ComposeView に置き換えて、

<!-- リスト部分 -->
<androidx.compose.ui.platform.ComposeView
    android:id="@+id/articleList"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:visibility="@{viewModel.isProgressBarVisible ? View.GONE: View.VISIBLE}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

ComposeView#setContent{} でJetpack Composeで作ったUIをセットしてあげるだけでした。

//  一時的に記事リスト部分のみJetpack Composeで実装する。
this.binding.articleList.setContent {
    ArticleList(viewModel = this.viewModel, onArticleClicked = this, onTagClicked = this)
}

(該当コード: main_activity.xml, MainActivity.kt)

このようなことができるので、新規で開発する部分やJetpack Composeの恩恵を受けながらリファクタリングしたい部分だけでも、かんたんに導入することができます。

参考: 相互運用 API | Jetpack Compose | Android Developers

おわりに

私たちBeta Computing株式会社は、石川県にあるスマホアプリ開発会社です。
Kotlin・Swiftによるスマホアプリ開発も、FlutterやXamarin.Forms、Unityによるクロスプラットフォームのスマホアプリ開発も、どちらも得意としているスマホアプリ開発のプロフェッショナルです。

Jetpack Composeに限らず、FlutterやSwiftUIに関しても幅広い知識を持っていますし、従来の開発手法に関しても確かな経験と実績があると自負しています。
スマホアプリ開発をご検討されているのでしたら、是非私たちBeta Computing株式会社におまかせください! 業種・ジャンル問わず対応可能ですので、ぜひご相談下さい。

Flutterでパズルを作ってみた話

弊社Android・クロスプラットフォーム担当のd-ariakeです。

弊社でもクロスプラットフォーム開発においてFlutterを採用しています。
今回はFlutter for Webでシンプルなパズルを作ってみたので、紹介させていただこうと思います。

作ったもの

このようなパズルを作ってみました。

flutter_puzzle

GitHub Pagesに置いてあります。
ぜひ遊んでみてください。
https://betacomputing.github.io/flutter_puzzle/

ソースコードはこちらに置いてあります。
https://github.com/BetaComputing/flutter_puzzle

Flutter for Webについて

3/4に Flutter 2.0 がリリースされましたが、その際にFlutterのWebサポートがstable化しました。
私もなにかFlutterでWebアプリを作ってみたいと思ったので、さっそくこのパズルを作ってみました。

特に手動で有効化設定を行わずともWebサポートが有効化されているので、

flutter create --org jp.co.betacomputing --platforms web ./flutter_puzzle

といったコマンドでWeb用のFlutterプロジェクトが作成することができます。

Webアプリ用ビルドは

flutter build web --web-renderer html

というコマンドで行うことができます。

上記ビルドコマンドにもオプションとして指定していますが、Flutter for Webには htmlcanvaskit の2通りのレンダリング方式があります。
それぞれダウンロードサイズの削減・他プラットフォームとの互換性とパフォーマンス性というメリットがあるようです。

ですが、現状はCanvasKitによるレンダリングにおいて正しく文字列が描画されないというバグがあるようなので、 --web-renderer html を指定したほうが良さそうです。

参考:

スライドアニメーションについて

このパズルを遊んでもらうとわかると思いますが、ピースを選択するとスライドアニメーションが再生されるようになっています。
これは AnimatedPadding を利用して実現させています。

パズルのピースを配置するコード は以下のとおりです。
各ピースの左と上のパディングを設定することによって任意の位置に配置するという実装アプローチを取っています。

//  パズルの複数のピースを生成する。
Widget _buildPieces(double pieceWidth, double pieceHeight) {
  return Stack(
    children: puzzle.pieces.map(
      (piece) {
        final offsetX = pieceWidth * piece.currentPos.hIndex;
        final offsetY = pieceHeight * piece.currentPos.vIndex;

        return AnimatedPadding(
          padding: EdgeInsets.only(left: offsetX, top: offsetY),
          duration: animDuration,
          child: SizedBox(
            width: pieceWidth,
            height: pieceHeight,
            child: _buildPiece(piece),
          ),
        );
      },
    ).toList(),
  );
}

あるピースがクリックされ、1つ右のマスに移動する状況を考えます。
右に移動するため、水平方向のインデックス piece.currentPos.hIndex の値が + 1 されます。
すると、 EdgeInsets.only()left には、更新前の値 offsetX よりも pieceWidth 分だけ大きな値が指定されます。
ここで AnimatedPadding を利用しているので、更新前と更新後の左パディングの値を線形補間しながら、なめらかに表示されるように描画してくれます。

更新後の値を渡すだけで、アニメーションの途中経過の面倒な座標計算を一切せずとも、Flutterが自動的にいい感じにしてくれました。 🙌

参考:

GitHub Pagesについて

最後にビルドとデプロイに関して軽く説明します。

見ての通り、このパズルはGitHub Pagesに配置しています。
タグをプッシュすると自動的にビルドが走り、ビルド内容が gh-pages ブランチにコミットされるように設定しました。

Release.yml:

  - name: Build (Web)
    run: flutter build web --web-renderer html

  - name: Deploy
    uses: peaceiris/actions-gh-pages@v3
    with:
      github_token: ${{ secrets.GITHUB_TOKEN }}
      publish_dir: ./build/web

gh-pages ブランチへの配置には peaceiris/actions-gh-pages@v3 というactionを利用しています。 GitHubのトークンとビルド先のディレクトリと指定するだけで、自動的にいい感じにしてくれます。 🙌

FlutterとGitHub Pagesを使えば、こんなに簡単にWebアプリが作れちゃいました。
今後も簡単なWebアプリであればFlutterで作ってみようと思います。 😤

おわりに

Beta Computing株式会社は石川県のスマートフォンアプリ開発に特化したソフトウェア会社です。 Kotlin・Swiftによるスマホアプリ開発も、FlutterやXamarin.Forms、Unityによるクロスプラットフォームのスマホアプリ開発も、得意としているスマホアプリ開発のプロフェッショナルです。

スマホアプリ開発をご検討されているのでしたら、是非私たちBeta Computing株式会社におまかせください! 業種・ジャンル問わず対応可能ですので、ぜひご相談下さい。

Xamarinの共有プロジェクトでも単体テストが書きたい!

弊社Android担当のd-ariakeです。

現在、Xamarinの改修案件をさせてもらっています。
そのプロジェクトで単体テストを書こうと思ったのですが、少しハマってしまいましたので、それを共有したいと思います。

このプロジェクトはXamarinネイティブの 共有プロジェクト でした。
共有プロジェクトは単なるソースコードの置き場で、AndroidとiOSのそれぞれのプロジェクトでそのソースファイルを参照するといった仕組みです。

この案件には既存のコードがあり、そのソースではがっつりそのままXamarin固有のコードが多用されていました。
ですので、そのまま .NET CorexUnit プロジェクトで共有プロジェクトの参照を追加してしまうと、Xamarin固有のコードを含むソースファイルでエラーになってしまいます。

↓ こんな感じに、Xamarin.AndroidプロジェクトとXamarin.iOSプロジェクトから見たときはビルドが通りますが、単体テストプロジェクトから見たときにエラーになります。

f:id:betacomputing3:20201124135257p:plain f:id:betacomputing3:20201124135309p:plain

単体テストでテストしたい部分というのはXamarinに依存しない計算ロジックや判定ロジックだったりします。
そのため、 共有プロジェクトをまるごと参照に追加するのではなく、ソースファイル単位での追加をすること で解決することができました。

↓ これです。

f:id:betacomputing3:20201124135316p:plain

既存のファイルの追加で、Xamarinに依存していないソースコードのみを単体テストプロジェクトに追加します。
もし、テストをしたい箇所でXamarin固有の機能 (Preferencesなど) を使用している場合、適切にリファクタリングをしてあげましょう。

固有の機能を必要とする箇所を IHogeHogeProviderIHugaHugaRepository などといったインタフェースで切り、テストをしたいビジネスロジックがインタフェースのみに依存するように変更します。

先ほどの画像でも Xamarin.Essentials.Preferences を使っている箇所がありましたが、 IHogePrefereces というインタフェースと HogePreferences (Impl) という具象型をつくり、単体テストプロジェクトには IHogePrefereces のみを追加しました。
これでちゃんと単体テストプロジェクトでもビルドが通るはずです。

そして、もう一箇所ハマったので、そちらも共有します。
私はテストに Moq というモックライブラリを使っているのですが、何も設定せずに使うと以下のようなエラーが発生しました。

System.ArgumentException : Cannot set up 〇〇 because it is not accessible to the proxy generator used by Moq: Can not create proxy for method 〇〇 because it or its declaring type is not accessible. Make it public, or internal and mark your assembly with [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] attribute, because assembly 〇〇 is not strong-named.

怒られている内容に従い、 AssemblyInfo.cs に以下のような属性を追加します。

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

しかし、最初は Hoge.SharedHoge.Shared.Tests というプロジェクトがあったとき、Hoge.Shared の方に書いてしまい、エラーが消えてくれませんでした。
共有プロジェクトは単なるソースコード置き場で、それを取り込む側の単体テストプロジェクトでアセンブリが吐かれるので Hoge.Shared.Tests の方に書かないといけません。 (ここで少しハマってしまいました。)
単体テストプロジェクトの方に書き直すと、正常にMoqによるモックが動作してくれました。 🎉

共有プロジェクトの保守をする際には是非参考にしてみてください!

先日のFlutterでのサンプルアプリをAndroidネイティブアプリでも作ってみた件

Android担当のd-ariakeです。
先日 弊社もFlutter採用致します! - Beta Computingブログ という記事の中で このような サンプルアプリを作り、紹介したかと思います。

今回はこのサンプルアプリと同じようなアプリをAndroidネイティブでも作ってみたので、設計の比較やポイントをご紹介したいと思います。

作ったアプリ

仕様は先日のものとほとんど変わっていません。

https://github.com/BetaComputing/SimpleQiitaClientAndroid

image

この同じような仕様のアプリをネイティブで作った際のポイントを軽く解説していきます。

ポイント

以下が今回説明するポイントとなります。

  • UIの設計
  • ネット上の画像のロード
  • FlexboxLayoutの導入

UIの設計

Flutter版では BLoC パターンで設計していましたが、Androidネイティブ版では MVVM パターンで設計を行いました。

UIはxmlで記述し、ViewModelにUIを制御するための変更通知機構を持ったプロパティ (LiveData) をもたせ、データバインディングによってViewModelのプロパティの状態をUIに反映させています。

internal class MainViewModel(/* 省略 */) : ViewModel() {

    //  取得中かどうか
    private val isFetching: MutableLiveData<Boolean> = MutableLiveData(false)

    //  記事リスト
    private val _articleList = MutableLiveData<List<Article>>()
    val articleList: LiveData<List<Article>> = this._articleList

    //  検索キーワード
    val keyword: MutableLiveData<String> = MutableLiveData("")

    //  検索ボタンが有効かどうか
    val isSearchButtonEnabled: LiveData<Boolean> = MediatorLiveData<Boolean>().also {
        val onChanged = { it.value = this.isFetching.value == false && !this.keyword.value.isNullOrEmpty() }
        it.addSource(this.isFetching) { onChanged() }
        it.addSource(this.keyword) { onChanged() }
    }

    //  プログレスバーを表示するかどうか
    val isProgressBarVisible: LiveData<Boolean> = this.isFetching

    //  検索ボタンがクリックされたとき。
    fun onSearchButtonClicked() { /* 省略 */ }

    //  検索を行う。
    private suspend fun search(keyword: String) { /* 省略 */ }
}
<layout <!-- 省略 --> >

    <data> <!-- 省略 --> </data>

    <LinearLayout <!-- 省略 --> >

        <com.google.android.material.appbar.AppBarLayout />

        <LinearLayout <!-- 省略 --> >

            <androidx.constraintlayout.widget.ConstraintLayout <!-- 省略 -->>

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/keywordEditText"
                    <!-- 省略 -->
                    android:text="@={viewModel.keyword}" />

                <com.google.android.material.button.MaterialButton
                    android:id="@+id/searchButton"
                    <!-- 省略 -->
                    android:enabled="@{viewModel.isSearchButtonEnabled}"
                    android:onClick="@{() -> viewModel.onSearchButtonClicked()}" />

            </androidx.constraintlayout.widget.ConstraintLayout>

            <ProgressBar
                style="@style/Widget.AppCompat.ProgressBar.Horizontal"
                <!-- 省略 -->
                android:visibility="@{viewModel.isProgressBarVisible ? View.VISIBLE : View.INVISIBLE}" />

            <androidx.recyclerview.widget.RecyclerView <!-- 省略 --> />

        </LinearLayout>

    </LinearLayout>

</layout>

例えば、検索ボタンの活性の制御の場合です。
以下のコードで、取得処理中かどうかを表すLiveDataと検索キーワードを表すLiveDataから、検索ボタンの活性に変換しています。

val isSearchButtonEnabled: LiveData<Boolean> = MediatorLiveData<Boolean>().also {
    val onChanged = { it.value = this.isFetching.value == false && !this.keyword.value.isNullOrEmpty() }
    it.addSource(this.isFetching) { onChanged() }
    it.addSource(this.keyword) { onChanged() }
}

そして、xmlのデータバインディング構文を使ってボタンの android:enabled プロパティに反映させています。

<com.google.android.material.button.MaterialButton
    android:id="@+id/searchButton"
    android:enabled="@{viewModel.isSearchButtonEnabled}" />

Flutter版では RxDartcombineLatest2() で変換した Stream/SinkStreamBuilder で実装していましたが、本質的には同じことをやっています。
このように主流の設計パターンが両者とも似ているので、Androidアプリ開発者がFlutterを学習するのはとても敷居が低いと感じました。

ネット上の画像のロード

Flutterではネット上の画像を表示させたいときは、単に

Image.network('URL')

というように書けばOKでした。
Androidネイティブには標準でそのような機能が用意されていので、Glide を使用しました。

Glide.with(context).load("画像のURL").into(imageView)

Glideを使うとこのように書くだけで、通信を行い、画像を反映させ、自動でキャッシュまで行ってくれます。
今回はデータバインディングを使った設計をしていたので、

@BindingAdapter("imageUrl")
fun ImageView.setImageUrl(url: String?) {
    Glide.with(this.context).load(url).into(this)
}

このような BindingAdapter を書いて、

<androidx.appcompat.widget.AppCompatImageView
    app:imageUrl="@{article.authorIconUrl}" />

xml上で画像の指定をできるようにしてみました。

このような処理はよく書くので、Flutterが公式で用意してくれているのはとてもありがたいですね。

FlexboxLayoutの導入

記事のタグを表示している部分は、動的に要素数を変えたり、横幅からはみ出た際に折り返しをしなくてはなりません。

Flutterだと Wrap を使うことで自動的に折り返しを設定してくれます。
Androidネイティブだとそのような機能が標準ではありません。

そこで FlexboxLayout というライブラリを使うことにしました。
使い方はとってもかんたんで <FlexboxLayout /> の下に <RecyclerView /> を入れてやり、FlexboxLayoutManager をセットしてやるだけです。

RecyclerView recyclerView = (RecyclerView) context.findViewById(R.id.recyclerview);
FlexboxLayoutManager layoutManager = new FlexboxLayoutManager(context);
layoutManager.setFlexDirection(FlexDirection.COLUMN);
layoutManager.setJustifyContent(JustifyContent.FLEX_END);
recyclerView.setLayoutManager(layoutManager);

引用元: https://github.com/google/flexbox-layout#flexboxlayoutmanager-within-recyclerview

しかし、Androidネイティブのリスト (RecyclerView) を扱おうと思うと、Adapterに関する部分のコードを書く必要があり、多少めんどくさいです。
そこで、コードの見通しを良くするために、カスタムViewを作ることにしました。

internal class TagList(/* 省略 */) : FlexboxLayout(/* 省略 */), TagClickedListener {
    internal var listener: TagClickedListener? = null
    private val adapter = Adapter(this)

    init {
        LayoutInflater.from(this.context).inflate(R.layout.tag_list, this, true)

        val layoutManager = FlexboxLayoutManager(this.context, FlexDirection.ROW)
        this.recyclerView.layoutManager = layoutManager
        this.recyclerView.adapter = this.adapter
    }

    internal fun setTags(tags: List<String>) { /* 省略 */ }
    override fun onTagClicked(tag: String) { /* 省略 */ }

    private inner class Adapter(private val parent: TagList = this) : RecyclerView.Adapter<BindingHolder>() {
        private val inflater: LayoutInflater by lazy { LayoutInflater.from(context) }
        var tags: List<String> = emptyList()

        override fun getItemCount(): Int = this.tags.size
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder =
            BindingHolder(TagBinding.inflate(this.inflater, parent, false))
        override fun onBindViewHolder(holder: BindingHolder, position: Int) {
            holder.binding.tag = this.tags[position]
            holder.binding.listener = this.parent
            holder.binding.executePendingBindings()
        }
    }

    private class BindingHolder(val binding: TagBinding) : RecyclerView.ViewHolder(binding.root)
}

@BindingAdapter("tags")
internal fun TagList.setTags(tags: List<String>?) {
    if (tags != null) this.setTags(tags)
}

@BindingAdapter("onTagClicked")
internal fun TagList.setListener(listener: TagClickedListener?) {
    this.listener = listener
}

TagList というカスタムViewとそのBindingAdapterを生やし、

<TagList
    <!-- 省略 -->
    app:onTagClicked="@{tagClickedListener}"
    app:tags="@{article.tags}" />

このようにxml中で使えるようにしてみました。

Flutterだと数行で実現できたことですが、Androidネイティブでやろうとすると、ものすごく大変でした。

感想

UIに関して、シンプルなUIを組むことに限って言えば、Flutterだとものすごく簡単に感じました。
Androidネイティブの場合、ActivityとFragmentの概念や、ライフサイクルの理解と画面の再生成の対応など、知っておかなくてはならないことがたくさんあります。
Flutterだとそれらを意識せずに作れてしまうので、アプリ開発初学者に対する敷居もかなり低くなっていると思います。

当然、Android固有の機能であったり、一部のUIに関してはAndroidネイティブが必要ですし、ネイティブがすぐに不要になるとは思いませんが、今後はFlutterの選択肢も増えていくと思います。

弊社もFlutter採用致します!

弊社Android担当のd-ariakeです。
この度、弊社もFlutterを採用することにしました。

ここ1週間ほど前からFlutterを入門していまして、最低限の動くものが作れるようになってきたので、ざっくりと学習メモのような感じで知見を残しておこうと思います。

サンプルアプリについて

Flutterの学習を進めつつ、シンプルなアプリを作ってみました。
リポジトリとアプリの動作イメージは以下の通りとなります。

FlutterQiitaClient - GitHub
image

Qiitaの記事検索APIを使用し、技術記事をキーワードで検索してリストで表示するといった動作となります。
(このイメージでは1つ記事しか表示されていませんが、実際は検索キーワードに応じた記事がちゃんと表示されるようになっています。)

ポイント

Flutterの入門をする上でポイントであると思ったのは以下の点です。

  • BLoCパターン
  • 静的解析
  • CI

これらについて軽く知見を共有していきます。

BLoCパターン

BLoC (Business Logic Component) パターンはFlutterでよく使われる設計パターンです。
実装をする際には以下のルールに従います。

  1. Inputs and outputs are simple Streams/Sinks only
  2. Dependencies must be injectable and platform agnostic
  3. No platform branching allowed
  4. Implementation can be whatever you want if you follow the previous rules

引用元: Flutter / AngularDart – Code sharing, better together (DartConf 2018)

これらのルールに従って実装したBLoCは以下のようになります。

class SearchPageBloc {
  SearchPageBloc(ArticleRepository repository) : this._repository = repository {
    this._searchEventSubject.listen((_) => this._search());
  }

  final ArticleRepository _repository;

  final _articleListSubject = BehaviorSubject.seeded(List<Article>.empty());
  final _isFetchingSubject = BehaviorSubject.seeded(false);
  final _keywordSubject = BehaviorSubject.seeded('');
  final _searchEventSubject = PublishSubject<void>();

  //  記事リストを通知するStream
  Stream<List<Article>> get articleList => this._articleListSubject.stream;

  //  取得中かどうかを通知するStream
  Stream<bool> get isFetching => this._isFetchingSubject.stream;

  //  検索キーワードを流すSink
  Sink<String> get keywordSink => this._keywordSubject.sink;

  //  検索ボタンが有効かどうかを通知するStream
  Stream<bool> get isSearchButtonEnabled => Rx.combineLatest2(
        this._keywordSubject,
        this.isFetching,
        (String keyword, bool isFetching) => keyword.isNotEmpty && !isFetching,
      );

  //  検索が要求されたことを流すSink
  Sink<void> get searchEvent => this._searchEventSubject.sink;

  void dispose() { /* 省略 */ }

  //  検索を行う。
  Future<void> _search() async { /* 省略 */ }
}

ルール 1. の通りに、BLoCからWidgetへの変更通知を行うもの (表示データや活性フラグなど) は Stream<T> で公開し、WidgetからBloCへの通知を行うもの (入力イベントなど) は Sink<T> で公開しています。

また、BLoC内部でのストリームの変換には RxDart を利用しています。
例えば以下の部分です。

//  検索ボタンが有効かどうかを通知するStream
Stream<bool> get isSearchButtonEnabled => Rx.combineLatest2(
      this._keywordSubject,
      this.isFetching,
      (String keyword, bool isFetching) => keyword.isNotEmpty && !isFetching,
    );

Rxの combineLatest2() を使い、

  1. 検索キーワードのストリーム (_keywordSubject)
  2. データの取得中フラグのストリーム (isFetching)

の2つのストリームから検索ボタンの活性状態に変換するようなストリーム (isSearchButtonEnabled) を作っています。

また、ルール 2. の通り、このBLoCは依存しているリポジトリ (ArticleRepository) を constructor injection で注入可能な仕組みになっています。
もしテストをしたければ、コンストラクタに Mockito などで作ったモックを渡してやることでかんたんに通信部分の挙動を変えることができます。

そして、このBLoCをWidgetに持たせる部分ですが、私は provider を使って実装しました。

//  検索ページ
class SearchPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Provider<SearchPageBloc>(
        create: (context) => SearchPageBloc(Dependency.resolve()),
        dispose: (context, bloc) => bloc.dispose(),
        child: _SearchPageContent(),
      );
}

create でBLoCのインスタンスを生成する処理を書きます。
コンストラクタに引数が必要なので Dependency.resolve<T>() という get_itラッパ を介して、リポジトリのインスタンスをサービスロケートしています。

(単体テストはドメイン層からBLoCやViewModelまでの部分に対して書くのが最も費用対効果が高いと思っているので、今のところ provider#create でのサービスロケータで問題はないと思っています。UIの自動テストをやろうと思った場合は別のアプローチが必要になるかもしれません。)

Stream<T> の変更に応じて、Widgetをリビルドする部分は StreamBuilder<T> を使います。
監視対象の Stream<T> と、その流れてきた値からWidgetをビルドする関数を渡してやります。

以下はプログレスインジケータを構築する部分のコードとなります。

//  プログレスインジケータを構築する。
Widget _buildProgressIndicator(SearchPageBloc bloc) => StreamBuilder<bool>(
      initialData: false,
      stream: bloc.isFetching,
      builder: (context, snapshot) =>
          snapshot.data ? const LinearProgressIndicator() : Container(),
    );

SearchPageBloc#isFetching: Stream<bool> を監視して、取得中ならば LinearProgressIndicator を返し、取得中ではなければ空っぽの Container を返すことで、プログレスインジケータの表示・非表示を切り替えています。

(Androidでは visibility = View.INVISIBLE というようにプロパティで制御をしていましたが、Flutterではまるごと再構築を行うことで制御するんですね。UIパーツが状態を持たないという考え方はちょっと新鮮です。)

というのが、BLoCパターンによる設計のポイントです。
他にもReduxやFluxなどもありますが、ひとまず標準的だと思われるBLoCパターンで実装を行っていました。
必要ならばこれらのアーキテクチャについても学んでおこうと思います。

静的解析

Dart & Flutterには標準で強力なフォーマッタ・Linterがついています。

analysis_options.yaml という名前で設定ファイルを置いておくと、その設定に応じた静的解析を行ってくれます。

このサンプルアプリでも設定をしています。
https://github.com/BetaComputing/FlutterQiitaClient/blob/master/analysis_options.yaml

Dartのフォーマッタを走らせるには以下のコマンドを実行し、

dart format --fix 

FlutterのLinterを走らせるには以下のコマンドを実行します。

flutter analyze

私は結構コードのスタイルを気にするタイプなのですが、これらを走らせることで誰か書いても良いコードスタイルになります。 そして、Dangerと組み合わせてCIで実行させることで、本質的でないコードレビューを避けることもできてとても良いですね。
みなさんもぜひ使いましょう!

CI

弊社では普段から GitHub を使っているので GitHub Actions でのCIも設定してみました。
設定ファイルは以下のような感じです。

CI.yml:

name: CI
# (※一部省略しています。)
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Setup Flutter
        uses: subosito/flutter-action@v1
        with:
          channel: 'stable'
          flutter-version: '1.22.2'

      - name: Restore dotenv
        run: echo ${{ secrets.DOT_ENV }} | base64 -d > .env

      - name: Restore dependencies
        run: flutter pub get

      - name: Build (Android)
        run: flutter build apk --debug

      - name: Test
        run: flutter test

      - name: Format and Report
        run: dart format --fix ./ > dart_format_report.txt

      - name: Analyze and Report
        continue-on-error: true
        run: flutter analyze > flutter_analyze_report.txt

      - name: Run Danger
        uses: danger/danger-js@9.1.8
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

flutter-action という便利なactionが公開されていたので、これを利用させてもらいました。
全体の処理の流れは以下のようになります。

  • Flutterのセットアップ
  • 環境変数 (flutter_dotenv) の復元
  • 依存関係の復元
  • ビルド (Android)
  • 単体テスト
  • フォーマット
  • Lint
  • Danger

フォーマッタとLinterを走らせて、その実行結果を Danger JS で報告させています。
(少し雑ですが dangerfile.ts も自分で書いてみました。)

もし、フォーマッタとLintで引っかかると以下のような怒られが生じてCIがコケます。
これで本質的なコードレビューに集中できると思います。

f:id:betacomputing3:20201020153915p:plain

このように、開発ツール・CIまわりについても基本的な使い方を学ぶことはできたかと思います。

入門してみた感想

1週間程度の駆け足で入門してみましたが、Flutterはそこそこ使えると感じました。
Dartの機能不足感を感じたり、一部だけ実現が難しいUIがあったりしましたが、基本的なマテリアルデザインのアプリならば十分だと思います。
開発ツールも公式が必要なものを提供してくれていますし、ホットリロードも爆速で開発体験も非常に良かったです。

ということで、Flutterでの開発もバリバリ対応できますので、ネイティブ (Kotlin・Swift)・クロスプラットフォーム (Flutter・Xamarin.Forms) 問わず、ぜひ我々にお任せください!
お仕事お待ちしております!