Next.jsのLayoutにpropsを渡したい

はじめに

BB.excite事業部でエンジニアをしている小川です。

はじめに注意書きですが、本記事ではNext.js 12.x の Layouts機能を取り扱います。

Next.jsのPages Routerには同じ要素を再利用するための機能としてLayoutパターンが用意されています。 また、Layoutはページ遷移時に再レンダリングが行われないようになっているため、状態を維持できるようになっていることが特徴的です。

nextjs.org

全てのページに共通して同一のレイアウトということであれば_app.jsなどにLayoutを適用すれば済む話ですが、大抵はページごとに多少の違いを持たせたくなるものです。

今回はレイアウトを変えるほどではないけれど、propsで多少の変更を加えたい!というときにpropsを渡す方法があるのでご紹介です。

Per-Page Layoutsを使ってみる

まずは公式ドキュメントで紹介されているベーシックなLayoutを定義します。

components/BasicLayout.jsx

import Navbar from './navbar'
import Footer from './footer'

export const BasicLayout = (page) => {
  return (
    <>
      <Navbar />
      <main>{page}</main>
      <Footer />
    </>
  )
}

定義したレイアウトを使うときは以下のような記述になります。

pages/index.jsx

import BasicLayout from '../components/basic-layout'
 
export default const Page = () => {
  return (
    /** Your content */
  )
}
 
Page.getLayout = (page) => BasicLayout(page) // ← ココ

レイアウトの定義をいくつかしておいて、呼び出し側でどのレイアウトを呼び出すか決めれば使い分けられて便利ですね。

propsを渡す

ここからが本題で、Layoutでpropsを受け取れるようにします。

components/basic-layout.jsx

import Navbar from './navbar'
import Footer from './footer'

export const BasicLayout = (page, props) => {
  return (
    <>
      <Navbar />
      <p>{props}</p>
      <main>{page}</main>
      <Footer />
    </>
  )
}

Layoutを呼び出す側では以下のように記述して渡します。 単純に第2引数で渡すだけですね。

pages/index.jsx

import BasicLayout from '../components/basic-layout'
 
export default const Page = () => {
  return (
    /** Your content */
  )
}

const props = "sample text"
 
Page.getLayout = (page) => BasicLayout(page, props)  // ← ココ

最近のドキュメントやNext.jsのexampleを見ていると以下のような記述もできるようです。

この辺りはお好みでしょうか。

pages/index.jsx

import BasicLayout from '../components/basic-layout'
 
export default const Page = () => {
  return (
    /** Your content */
  )
}
 
Page.getLayout = (page) => {
  <BasicLayout>
    <p>propsを渡さなくてもちょっとした内容であればここに書ける</p>
    {page}
  </BasicLayout>
}

初めてのオフライン展示会からデザイナーが学んだこと

こんにちは! SaaS・DX事業部デザイナーの鍜治本です!

技術ブログとは毛色が少し異なりますが、エキサイトのSaaS事業部としてオフラインの展示会に出展し、デザイナーとして体験したことをさっくり書き起こします!

出展したイベント概要

2日目(11/22) ブースの様子
今回はパシフィコ横浜で開催された「BOXIL EXPO IT・DX展 in TOKYO 2023」に2日間出展していました。 過去には福岡、今回初めて横浜で開催され、出展した企業数はおよそ70社。各社ブースが思い思いの装飾を施し開催されました。

イベントサイトはこちら event01.expo.boxil.jp

展示したもの

今回の出展ではIT・DX分野を取り扱う展示会であるため、SaaS・DX事業部の経営管理クラウドツール「KUROTEN」と、エキサイトで現在注力している「生成系AI開発支援」を展示し、幅広い業界・職種の来場者にアプローチしました!

KUROTENについてはこちら kuroten.biz

エキサイトのAI事業についてはこちら www.excite-holdings.co.jp

ブース運営と来場者の反応

出展するまでの間、デザイナーとしてブース装飾アイテムの準備や、配布するパンフレットの作成しました。が、話し出すとキリがなくなるので、この記事では実際の会場について触れます。

経営管理ツールとAI開発支援への反応の違い

ブース内に設置したパネル(KUROTEN事業とAIソリューション事業)
KUROTENのインサイドセールスやフィールドセールスが苦戦していると聞いていた通り、KUROTENは来場者への声かけの反応が得られにくい印象でした。元々「経営企画」といった少ないターゲットを指していることもあり、改めて「予実管理SaaS」の間口の狭さを体感しました。
逆に「経営企画」を主業務としている方が、自らブース訪問をしてくださるなど、課題や悩み自体は顕在化していてたどり着くまでの道のり開拓が課題であると再認識しました。

一方で、生成系AIの開発支援は業界・職種を問わず幅広に興味を持っていただけていました。お話を聞く中には「昨今のAI技術活用の波で自社も何かしたい」「どんな方法があるか情報収集に…」など、立ち話程度で聞いていただいた方から温度感高めの方までいらっしゃり、話題性が高いゆえ広範囲の興味が寄せられていました。

デザイナーの視点から見た展示会の雰囲気

本来であれば「ザ・営業」をする展示会。今回は出展準備に関わっていた関係で、当日のフロント営業にも挑戦しました。会場で営業したり来場者と話してみた感想を、デザイナーなりに綴ってみます。

とにかく声をかけまくるフロント営業

通りかかる来場者に、とにかく声をかけまくります。来場者は我々に興味を持ってきているわけではないので、『いかにキーワードを耳に入れてもらえるか』『目に入った情報から興味を持ってもらえるか』が鍵となります。
元営業経験者からのアドバイスによると「とにかく目を見て声をかける」がコツ。そのコツに合わせてサービスのキーワードになりそうな「予実管理」「生成系AI」「エキサイトのtoB事業」を通りかかる人に向けて連発し続けます。

ちょっと見切れて写ってました
興味を持ってもらえたら、パンフレットを見せながら「どういったサービスなのか」「何が解決されるのか」を簡単に伝えます。来場者の所属や担当業務など伺いながらトークに要素を混ぜてゆき、「あるある、わかる」「言われてみればそうかも」と共感を引き出します。もう少し聞いてみようかな?と思ってもらえるように、相手が引かない距離感での会話を意識していました。
さらに興味を持ってもらえたら、デモ実演担当者まで繋ぎ、より具体的なペインの聞き出し、最終的にアポ獲得を目指すフローを回します。
大多数の来場者はブース前を通り過ぎるので、聞いてもらえない悔しさを感じましたが、いざ聞いてもらえた時には半端ない嬉しさがありました。

プレゼンしながら興味を引き出す

事前に配布用のパンフレットをカツカツスケジュールで大急ぎ作成しており、これを使って来場者にサービスの説明をします。
資料には自分が話す時のヒントが散りばめられていますし、興味を持った人が聞くときの視覚情報を補完してプレゼンができます。

実際に配布していた折パンフレット
自社サービスの説明やセールスポイントが頭で分かってはいるものの、いざ話そうとすると緊張で頭もこんがらがるもの。今回作成したパンフレットには、サービスの特徴・使用後のイメージ・導入事例…など記載していたため、話の糸口を引き出しながら初めての営業ができました。

達成度と反省点

今回2日間で来場した総数のうち、名刺交換や情報交換できた数(リード数)がおよそ15%、さらにアポに繋がったのがリードのうち2%という結果に。
一般的に展示会開催後のリード数は5~10%で、商談化につながるのはそのうちの1〜5%と言われています。今回の結果と比較しても、ブースでの営業やデモ実演によってより多くのリード獲得ができたこと、そしてリードからアポに繋げられました。普段の業務ではなかなか経験できる事柄では無いので、とても新鮮な経験を積めたと感じています。

一方で、エキサイトの認知度は「toC事業」によるものが多く、話しているほとんどの方々が「ポータルサイトやメディアじゃないの?」とおっしゃっていました。まだまだtoBSaaSをやっている認知は届いておらず、広める活動をする必要があると痛感しました。

さらに、展示会自体も初めて出展したことから、準備が行き届いていないこと、計画もままならずスケジュールがカツカツしていたこと、他社ブースと比較すると垢抜けなさがありました。例えば、通路側にディスプレイを設置しプロダクト紹介動画を流していたのですが、実際のツール画面をベースとした説明で初見者には分かりにくいものでした。

(ちょうど正面ブース「リーガルフォース」さんは、こんな感じの動画を流していました。)
youtu.be
他社ブースを見ると、アニメーションで課題解決までを簡潔にまとめているものや、ストーリーベースで課題を提示したり、理想のゴールイメージまで見せているものなど、サービス自体にまだ興味がない人が見るアプローチで作られていました。
デザイナーとしてこだわれるならもっとこだわりたい思いも強く、日頃から色々なアウトプット方法を模索し、作成し続けなければならないと思えた刺激的な1日であったと感じています。

まとめ

今回は初めての展示会出展で、まだまだ改善でる箇所や反省点がいくつかあります。
ただ、デザイナーとして普段体験できないことを一気に吸収できたので、これらを踏まえてもっとSaaS・DX事業部全体を盛り上げられるようにデザインで貢献したいです。

そんなエキサイトの、デザイナーやエンジニア(もちろんビジネスも!)を盛り上げてくれるメンバーを、新卒・中途問わず募集しています!
カジュアル面談も随時受付可能ですので、お気軽にお声かけください!🙆 recruit.jobcan.jp

新しく環境を作るために必要なざっくりとしたAWSの知識

はじめに

こんにちは。新卒1年目の岡崎です。

今回、初めて内部用のAPIのための環境を作成しました。その時に必要だった知識も分からずに始めたので、その備忘録として記事にまとめます。初めてAWSで環境を作るよ!という人や、まず何から初めて見ればいいのか分からない……といったような人の手助けになれば幸いです。

環境

大まかな流れ

ユーザーがURLでアクセスしてから、レスポンスを返すまでの大まかな流れを解説します。

  1. ユーザーがURLでアクセスする
  2. ALBがターゲットグループで振り分け
  3. コンテナ化されたアプリケーションが処理を行い、ユーザーにレスポンスを返す

それでは、これを実現するためには何の設定が必要なのでしょうか。

必要なAWSの設定

内部用のAPIの環境が必要だった設定を記しておきます。

今回は下記のサービスを使いましたが、作りたいものによっては他のサービスも検討してください。

Route 53

Route 53では、ドメインの作成を行うことができます。ドメインを作成することで、ユーザーはアクセスすることができるようになります。

ALB

ALBでは、設定したターゲットグループごとに振り分けを行います。サーバーにかかる負荷を均等に分散する役割を担います。

EC2

EC2は、AWS上に構築できる仮想サーバーの一つで、クラウド上で簡単に立ち上げることができます。

ECR

ECRでは、Dockerイメージの保存・管理を行います。リポジトリの作成、Dockerイメージのプッシュ/プル、Dockerイメージの詳細確認をすることができます。

ECS

ECSは、コンテナを管理するためのフルマネージドなAWSのサービスです。ここではECRで作成したDockerイメージからコンテナを起動することができます。

IAMロール

IAM ロールでは、AWSのサービスへのアクセス権限などを設定することができます。

最後に

数日前の私が必要だったAWSのざっくりとした知識をまとめました。正直、もっと早く勉強していれば良かったと思っていますが、学べる機会があったので良かったと思っています。もっと精進します……。

ここまで読んでいただきありがとうございました。

デザインのクオリティを上げる一手間!デザインTips集第二弾

はじめに

こんにちは、エキサイト株式会社3年目デザイナーの山﨑です。

今回は、デザインのクオリティを上げる一手間!デザインTips集第二弾を紹介したいと思います!

▼前回の記事はこちら▼

tech.excite.co.jp

意外と簡単にできるものばかりなので、ぜひ実践してみてください🙌

人物の背景に図形を組み合わせる時は、人物の頭を少しだけ出すと遠近感が生まれる

ウェビナーなどでよく使われるのですが、登壇者紹介時のデザインは人物の頭を少しだけ出してあげたり何かポーズをとっている場合はポーズをとっている手を出してあげたりすると遠近感が出て画面が少し賑やかになります。

人物と写真の背景を組み合わせる時は、人物により目がいくようにするため背景を少しだけぼかす

少しわかりづらいですが、画像のように切り出した人物に背景を組み合わせる場合は背景のみぼかしてあげると人物がくっきりと浮かび上がります。人物と背景の間に影を入れてあげるとより効果的です。

影の色は黒ではなく、背景写真の一番濃い色(この場合はネイビー)にすると馴染みやすいです。

Canvaの「ぼかし」機能を使って簡単に立体感を出せたり、人物だけ切り出さなくても「オートフォーカス」機能で背景のみぼかすことができるので非常におすすめです!

エフェクトを配置する時は対角線上に配置する

画面をより豪華に盛り上げたいときに使うキラキラは、対角線上に配置するとバランスよくまとまることが多いです。

その際にキラキラの大きさをほんの少しだけ変えてあげると単調にならない画面作りができると思います!

最後に

いかがだったでしょうか?

必ずこれが正解とも言えないのですが、最後のデザインの仕上げとして使えるので、ぜひ活用してみてください🙏

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があればぜひぜひ連絡ください!🙇

www.wantedly.com

Spring Securityで、同一ドメインにて別セッションでログイン管理をする方法

こんにちは。 エキサイト株式会社の三浦です。

Spring Securityでセッションを使ってログイン管理をする場合、基本的にはドメイン単位でセッションが分かれます。

今回は、同一ドメインにて別セッションでログイン管理する方法を説明します。

1. プロジェクトを分ける

まずは、セッションを分けたいサービス同士を別プロジェクトとして作成します。

Nginx等のプロキシを使って、同一ドメインにアクセスが来てもパスなどをもとにリクエストが振り分けられるようにしておきましょう。

2. セッション用Cookie名を別々にする

次に、セッション用のCookie名を別のものにします。

この設定が肝で、これによって同一ドメインでも別セッションとして管理できるようになります。

Spring SecurityをMVCで扱っていれば、 application.yml に以下の設定を書くだけでCookie名を変更することができます。

server:
  servlet:
    session:
      cookie:
        name: CUSTOM_COOKIE_NAME

spring.pleiades.io

セッションを分けたいプロジェクト間で、別々の名前を設定しましょう。

これで、同一ドメインでも別のログインセッションとして扱われるようになりました!

終わりに

同一ドメインで別セッションのログイン管理をしたい、という需要はそこまで多くないと思いますが、いざそうなった時に使える情報ではないでしょうか。

またそれ以外にも、何かしらの事情でセッション用のCookie名を変えたい場合にも使えるでしょう。

そういった際に、何かしらの役に立てば幸いです!

タスクランナーをgo-taskにする

エキサイト株式会社メディア事業部エンジニア佐々木です。開発で使用するタスクランナーは、一般的にMakefileを使うものが多いかと思います。より簡単なgo-taskを紹介いたします。

インストール

Macの場合は、Homebrewを使用しインストールします。

brew install go-task

他のOSは、公式ドキュメント を参照してください。

タスクファイル

Taskfile.ymlを作成します。Makefileのようなものです。

YAML形式ですので、視認性が良いです。

version: '3'
tasks:
  helloworld:
    desc: hello world
    cmds:
        - echo 'hello world'
        - echo 'hello world' > output.txt

  helloshell:
    desc: hello shell
    cmds:
        - echo 'helloshell'
    silent: true

上記のように視認性が良いです。

実行

設定したタスクを実行してみます。

$ task helloworld helloshell 

task: [helloworld] echo 'hello world'
hello world
task: [helloworld] echo 'hello world' > output.txt
helloshell

正常に動作しています。設定で silent: true を設定することで、実行時の標準出力から task: xxx がなくなって結果のみになります。

タスク一覧

Makefileでは、呪文をかかないとタスク一覧がでてきませんでしたが、go-taskでは、タスク一覧を表示してくれるものが標準で備わっています。

$ task -a


 task -a
task: Available tasks for this project:
* helloshell:                 hello shell
* helloworld:                 hello world

desc も記載しておくことで、説明も出力してくれます。

まとめ

Makefileより視認性も使いやすさもあると感じました。標準導入に向けて活動していこうと思います。

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

FlutterKaigi 2023に参加してきました。

はじめに

エキサイト株式会社の高野です。

今回はFlutterKaigi 2023に参加してきましたのでその感想及びレポートになります。

聴講したセッション一覧

  • Flutterアプリにおけるテスト戦略の見直しと自動テストの導入
  • 出前館におけるFlutterの現在とこれから
  • Master of Flutter lifecycle
  • Dartのコード自動生成のパッケージを自作する方法について
  • Flutterで構築する漫画ビューア
  • 魅せろ!Flutterで目を惹くUIデザインを実装する

Flutterで構築する漫画ビューアに関しまして弊社の克さんが発表してくださいました。

補足としてのブログがありますのでぜひご一読ください。

tech.excite.co.jp

感想

詳しくはアーカイブを見ていただけたらと思います。

個人的には出前館様のFlutterでリプレイスを行った話が興味深かったです。

出前館様はあまり他に例を見ない韓国チームを含んだ上でのプロダクトということでした。 日本語をネイティブとしないメンバーがいる中で、英語での進捗確認だったり、チームビルディングに力を入れているそうです。

また、その後のテスト戦略としての話はあまりテストを書いてきていない自分にはとてもためになる内容でまたアーカイブをもとに参考にしたいと思います。

そしてもう一つ、AAkira様のFlutterのライフサイクルの話です。

下記にスライドリンクを貼りましたのでぜひご一読いただいて欲しい良い資料でした。

あまり自分で実装する際に意識していなかった

  • 画面のライフサイクル
  • アプリのライフサイクル
  • 遷移のライフサイクル

この部分について詳しく知ることができました。

ライフサイクルは、プロダクト開発でログを計測する際によく使います。このスライドをもとにもっと自身のプロダクトにおいて柔軟にライフサイクルの検知及びログの送信を実装できるように改善していきたいと思います。

speakerdeck.com

最後に

全部のセッションがすぐにためになるわけではないですがしっかりとアーカイブを見た上でさらにFlutterへの深みへと進んでいきたいと思いました。

このようなイベントを企画・運営してくださった運営の皆様、ありがとうございました。

採用情報

エキサイトではエンジニアを随時募集しています。 興味がありましたらお気軽にご連絡いただければ幸いです。

www.wantedly.com

FlutterKaigi 2023 参加レポート

エキサイト株式会社の@mthiroshiです。

FlutterKaigi 2023に参加してきましたので、その内容をレポートします。

FlutterKaigi 2023の概要

FlutterKaigi とは、FlutterやDartの知見を共有する技術カンファレンスです。 flutterkaigi.jp

会場は株式会社ナビタイムジャパン様にて行われました。

スタート前の会場の様子

聴講したセッション

セッション数は基調講演を含めて21セッションありました。

聴講したセッションについて紹介します。

基調講演「Flutter's 8 years journey」

Flutter GDEの方から、Flutterの8年間の歩みについてお話しいただきました。

Flutterは2015年にDart Developer Summitにて発表されました。 その後、Dartが開発言語として進化するとともに、FlutterもUI フレームワークとして進化してきました。

Flutterは、モバイルアプリの開発においては既に習熟してきています。今後の展望としては、Flutter WebやDesktopアプリといったモバイル以外の領域も充実していくであろうとのことでした。 また、Casual Games Toolkit のゲーム開発や、3Dレンダリングサポートの話もあり、Flutterが更に幅広く使われていく進化が期待できると思いました。

私がFlutterを触り始めたのは2022年からだったので、Flutter登場初期の話は興味深かったです。

「Flutter アプリにおけるテスト戦略の見直しと自動テストの導入」

WINTICKET のテスト戦略の見直しと改善事例の紹介でした。

speakerdeck.com

過去の障害事例から改善点を考えたり、テストピラミッド、テストマトリクスを用いて、体系的に現状を分析し、注力する部分を導いていく内容でした。

まとめとして、どのようなアプローチを取るべきか、まずは現状のプロダクトやチーム体制を分析することが重要であることを挙げていました。 また、導入コストが高いことや効果の実感までに時間を要することを挙げており、テスト設計に取り組むべきと思いつつも、ハードルが高い点は検討すべきと感じました。

「詳解!Flutterにおける課金実装」

TOKYO MIX CURRYのアプリ決済についての事例でした。 TOKYO MIX CURRYとは、アプリでしか買えないカレーを提供するサービスであり、実店舗におけるアプリ決済の具体的なシステム構成や陥った課題についてお話し頂きました。

システム構成としては、アプリ、サーバーサイドが登場する一般的な構成に加えて、決済端末であるSquareリーダーやレシートをプリントするためのEpsonといったハードウェアとも連携する仕組みになっています。

実際にあったユーザーの意図しない操作や外部SDK連携で困ったことがあり、それらを解決した方法について紹介頂きました。

DartによるBFF構築・運用 〜Dart Frog×melos〜」

株式会社ゆめみさんのDartによるBFFの事例について紹介頂きました。

speakerdeck.com

BFFは、一般的にクライアントサイドが開発することが多く、今回はクライアントサイドがFlutterアプリだったので、DartでBFFを構築することに至ったとの事でした。 サーバーサイドのDartフレームワークには、いくつか選択肢がありますが、今回は軽量かつ比較的新しいDart Frogを採用したそうです。

その他、Melosというパッケージを使ったマルチパッケージ構成(モノレポ)の話や、BFFとアプリのAPIレスポンスの型を共通化の話もあって興味深かったです。

Dartのコード自動生成の仕組みと、コード自動生成のパッケージを自作する方法について」

freezedやretrofitといったコードの自動生成をするパッケージが行っている自動生成の仕組みについての内容でした。

www.slideshare.net

サンプルプロジェクトが公開されているので、スライドと合わせて理解を深めていきたいです。 github.com

「Flutterで構築する漫画ビューア」

弊社所属のアプリエンジニアである katsuさんが、漫画ビューアの実装について登壇しました。

speakerdeck.com

内容の補足についてこちらの記事で紹介しております。 tech.excite.co.jp

「我々にはなぜRiverpodが必要なのか - InheritedWidgetから始まるappstate管理手法の課題」

Flutterにおける状態管理の仕組みと課題、解決策についての紹介でした。

docs.google.com

Riverpodでは、状態管理が持つ多くの課題に対して解決策を用意しており、利用する側はその解決策を理解しておく必要があるとのことでした。 会場でも多くの方がRiverpodを使っていると手を挙げており、興味関心が高いセッションだったと思います。

ノベルティー

ノベルティーグッズやスポンサー企業様のステッカーを頂いてきました。 ステンレスマグカップがかっこよかったので、使っていきたいと思います。

ノベルティーのステンレスマグカップ

おわりに

久々にオフライン開催のカンファレンスに参加してきました。

オフラインならではの会場の空気感や懇親会が楽しく、有意義なカンファレンスを体験できました。 運営して下さったFlutterKaigiスタッフの皆様、各スポンサー企業様、ありがとうございました。

採用アナウンス

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

Spring Securityで、独自ユーザ情報を簡単にHTML上に表示する方法

こんにちは。 エキサイト株式会社の三浦です。

Spring Securityを使えば、Spring Bootで簡単にログイン機構を作ることができます。

今回はSpring Securityの一部機能を拡張し、独自のユーザ情報を簡単にHTML上に表示する方法を紹介します。

Spring Securityのデフォルトユーザ情報

Spring Securityでは、多くの場合デフォルトで以下の3つのユーザ情報を持つことになります。

  • ユーザ名(IDとして用いるもので、メールアドレス等を持たせることもできる)
  • パスワードのハッシュ値
  • 権限一覧

以下のようにすることで、Spring Securityにユーザ情報を渡すことができます。

package sample;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // ユーザ名からパスワードのハッシュ値や権限一覧を取得する
        // ここでは、passwordHash と roleList とする

        return new User(
                username,
                passwordHash,
                roleList
        );
    }
}

なお、返り値である User クラスは返り値の型として指定されている UserDetails インターフェースの実装クラスで、Spring Securityにおけるユーザを示しています。

渡した情報は、ThymeleafというHTMLテンプレートと、その拡張機能である thymeleaf-extras-springsecurity6 を使えば、以下のようにHTML上に表示できます。

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6"
      lang="ja"
>

<body>
    <!-- Spring Securityからユーザ名を取得して表示する -->
    <div sec:authentication="name"></div>
</body>
</html>

実際の画面が以下になります。

なお今回は、ユーザ名としてメールアドレスを渡すようにしています。

想定通り、メールアドレスを画面上に表示することができました。

必要な情報がこれだけならいいのですが、場合によっては、例えば独自に定義したユーザコードなど、上記以外の情報もHTML上で使用したいこともあるでしょう。

Spring Securityでは、それも簡単に実装することができます。

独自のユーザ情報を表示する

まずは、独自のユーザ情報をSpring Securityに渡します。

今回は、 userCode という独自データを持たせることにします。

先程使っていたSpring SecurityデフォルトのUserクラスを拡張して、 userCode フィールドを持たせるようにします。

package sample;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

public class UserWithCode extends User {
    private final String userCode;

    public UserWithCode(String username, String password, Collection<? extends GrantedAuthority> authorities, String userCode) {
        super(username, password, authorities);
        this.userCode = userCode;
    }

    public String getUserCode() {
        return userCode;
    }
}

そしてこれを返り値として返すようにします。

package sample;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // ユーザ名からパスワードのハッシュ値や権限一覧を取得する
        // ここでは、passwordHash と roleList とする

        return new UserWithCode(
                username,
                passwordHash,
                roleList
                "customUserCode"
        );
    }
}

これで、Spring Securityに独自情報である userCode を渡すことができました。

後はHTML上で以下のように参照するだけです。

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6"
      lang="ja"
>

<body>
    <div sec:authentication="name"></div>

    <!-- 独自情報はprincipal内に保存されている -->
    <div sec:authentication="principal.userCode"></div>
</body>
</html>

無事表示できました!

最後に

画面の表示はもちろん、URLの組み立てや分岐などでも独自情報を使うことはあるかもしれません。

非常に便利な機能なので、ぜひ使っていきましょう!

FlutterKaigi 2023 - Flutterで構築する漫画ビューア 補足

こんにちは。エキサイト株式会社 モバイルアプリエンジニアの克です。

このたび、FlutterKaigi 2023に登壇しました。

fortee.jp

speakerdeck.com

今回は、当日お話しできなかった内容をいくつか補足したいと思います。

画像の保存場所

発表では画像の保存場所としてアプリケーションのディレクトリを使用しましたが、実際にはもう少し検討する必要があります。
大まかにいうと、AndroidiOSも、アプリケーションディレクトリとキャッシュディレクトリの2箇所が候補になります。
アプリケーションディレクトリはアプリのデータの一般的な保存場所で、ここに保存したファイルは自動的にバックアップされます。再取得が可能なデータはバックアップするべきではないので、漫画の画像ファイルはバックアップから除外したほうがいいでしょう。
キャッシュディレクトリはキャッシュ用の保存場所で、ここに保存したファイルはバックアップされることはありません。ただし、OSによってファイルが削除される場合があるため、ファイルが存在するかを確認しながら運用する必要があります。
アプリケーションディレクトリで画像ファイルを自動バックアップから除外して運用するか、キャッシュディレクトリで常にファイルの確認をしながら運用するかというのがいいのかなと思います。

ImageProviderの進捗管理

画像を読み込んでいる間の進捗表示は、ImageのloadingBuilderで実装することができます。
この際に、進捗を表すImageChunkEventはImageProviderから提供されます。
ImageProviderを自作する場合は、このImageChunkEventの管理も行う必要があるため、忘れずに実装するようにしましょう。

edgeToEdgeのOSバージョン対応

SystemUiModeのedgeToEdgeは、Android 10以降でのみ使用可能なモードです。そのため、それ以下の場合には別の対応をする必要があります。
しかし、現在のFlutterではこれ以外の方法で操作をする方法が見つからず、ネイティブ側での実装が必要になるかもしれません。
また、対象のOSバージョンは古く今後も利用率は下がっていくため、edgeToEdgeが利用できない場合にはmanualでシステムUIを出しっぱなしにするという選択もありなのかなと思います。

見開き表示の実装方法

今回の発表では2枚の画像を連結して1枚の画像にしましたが、やり方によってはウィジェットを2つ並べて表示すれば画像を連結しなくても見開きページを実現することはできるかもしれません。 元のサービスの仕様で、画像を切り取って共有することができるというものがありました。そのため、1枚の画像として扱えたほうが都合が良かったのでこういった実装にしたという経緯もあります。

さいごに

やはりオフラインでのイベントは熱意と雰囲気が直に伝わってきますし、人と人との繋がりもあって非常にいいものだなと思いました。 イベントの開催にご尽力いただきました運営の皆様、セッションをお聞きいただいた皆様、ありがとうございました。

採用情報

エキサイトではエンジニアを随時募集しています。 興味がありましたらお気軽にご連絡いただければ幸いです。

www.wantedly.com

MermaidでER図を書く

はじめに

こんにちは。新卒1年目の岡崎です。

今回はテーブル定義をする時に、使うと便利かもしれないMermaidの紹介をしていきます。

Mermaidとは

Mermaidは、図を動的に作成・変更するJavaScriptベースの図形作成およびグラフ作成ツールです。もっと詳しく知りたい人はここを見てください。

Mermaidを使うと、ER図を簡単に書くこともできます。

使い方

今回は例として、4つ紹介します。

VSCode

VSCodeでは、拡張機能が存在しています。これをインストールしてください。

IntelliJ IDEA

IntelliJ IDEAでも、VSCodeと同様に拡張機能が存在します。これをインストールしてください。

Notion

NotionでもMermaidが使えます。Notionでコードブロックを作ると、その中でMermaidが選択できます。

GitHub

GitHubでも、以下のようにコードブロックを作ると使うことができます。

Mermaidの記法

実際の記法の紹介をして行きます。

テーブル定義

Mermaidの記法を使って、ユーザーテーブルを作ると以下のようになります。

erDiagram
 
  user {
      bigint user_id PK "ユーザーID"
      VARCHAR(255) name "ユーザー名"
  }

動的に生成された図は、以下です。

リレーション定義

Mermaidの記法を使って、カーディナリティを表現すると以下のようになります。

左の記述 右の記述 意味
|o o| 0 or 1
|| || 1
}o o{ 0以上
}| |{ 1以上

実際にリレーションを表現すると、以下のようになります。

erDiagram
  A ||--o| B : "1対0or1" 
  A ||--|| C : "1対1"
  A ||--o{ D : "1対0以上"
  A ||--|{ E : "1対1以上"

動的に生成された図は、以下です。

これらの表現を組み合わせてER図を表現します。

Mermaidでは上部にリレーション、下部にテーブル定義を定義することが多いです。例として、userとarticleのテーブルを定義してみました。この時、ユーザーは1つ以上の記事を持ちます。

erDiagram
  user ||--o{ article   : "1対0以上"

  user {
      bigint user_id PK "ユーザーID"
      VARCHAR(255) name "ユーザー名"
  }

  article {
      bigint article_id PK "記事ID"
      VARCHAR(255) title "記事タイトル"
      text text "記事本文"
      bigint user_id "ユーザーID"
  }

動的に生成された図は、以下です。

最後に

Mermaidを使ってER図を書くことは、先輩から新卒研修で教えてもらいした。

ER図を書くときにとても便利なので、今でも好んで使っています。みなさんもぜひ使ってみてください。

エキサイトブログにおけるPostgreSQLのイベントトリガの活用例

はじめに

エキサイト株式会社 バックエンドエンジニアの山縣(@zsp2088dev)です。

エキサイトブログでは、DBコスト削減に取り組んでおり、これまでにいくつかの改善を行ってきました。 取り組んだこととその効果については、下記記事をご参照ください。

tech.excite.co.jp

その記事の中で、PostgreSQLのイベントトリガの活用について簡単に触れました。 本記事では、イベントトリガのより具体的な活用方法について紹介します。

エキサイトブログタグ機能

エキサイトブログには、1つの記事に最大で3つのタグをつける機能があります。 記事にタグをつけると、人気タグランキングに参加できたり、タグ検索をしたりできます。 また、ブロガーのトップページには、「タグとタグの個数」を表示する箇所があります。

ここで、タグを扱うDBのテーブルに着目します。 一部カラムは省略・改変していますが、ユーザーID、記事ID、1つ目のタグ、2つ目のタグ、3つ目のタグ を持つテーブル構成となっています。

以下のクエリーを実行すると、ブロガーの記事に紐づくタグを取得できます。

SELECT
    tag_name_1,
    tag_name_2,
    tag_name_3
FROM
    sample_tag
WHERE
    user_id = 'test_user_1'
    AND article_id = 1

上記のように、1つ目のタグ、2つ目のタグ、3つ目のタグとカラムを持っているため、ユーザーIDと記事IDを指定すると簡単に記事に紐づくタグを取得できます。 一方で、「タグとタグの個数」を取得するためには、以下のクエリーを実行しないといけません。

SELECT
    tag_name,
    COUNT(*) AS tag_count
FROM (
    SELECT
        user_id,
        tag_name_1 AS tag_name
    FROM
        sample_tag
    UNION ALL
    SELECT
        user_id,
        tag_name_2 AS tag_name
    FROM
        sample_tag
    UNION ALL
    SELECT
        user_id,
        tag_name_3 AS tag_name
    FROM
        sample_tag) sample_tag_name
WHERE
    user_id = 'test_user_1'
    AND tag_name <> ''
GROUP BY
    tag_name
ORDER BY
    tag_count DESC

これにより、これまでに多くの記事を書き、様々なタグを登録しているブロガー記事を閲覧したときのクエリーの負荷が非常に高くなっていました。 キャッシュを利用してDBの参照頻度を減らすようにしていましたが、キャッシュ生存時間が切れてしまうとやはり負荷が増加してしまいます。 そのため、何らかの方法で根本的な解決をする必要がありました。

PostgreSQLのイベントトリガを採用した理由

上記の状況のときに、1つ目のタグ、2つ目のタグ、3つ目のタグと持つテーブルに対して正規化を行い、「タグとタグ順序」を持つようなテーブルに切り替えるのが望ましいと思います。実際に、エキサイトブログのブログテーマ機能では、旧テーブルから新テーブルにマイグレーションを行っています。

tech.excite.co.jp

今回はブロガー記事の閲覧に問題が生じていため、迅速な対応が求められていました。 タグテーブルの修正による影響範囲は大きく、旧テーブルから新テーブルへのマイグレーションをするには時間がかかるため別の解決方法を模索する必要がありました。

また、今回特に問題となっているのは、ブロガーのトップページに表示するメニューだけであり、この問題の解決が最優先でした。

チームメンバーとの話し合いの結果、PostgreSQLのイベントトリガ機能を利用すれば、この問題を解決できる可能性があるという結論に至りました。

実際に作成したテーブルとイベントトリガ

イベントトリガを活用するにあたり、以下のような「タグとタグ個数」テーブルを用意しました。

このテーブルに対して、以下のクエリーを実行すると、UNION ALLを使用したクエリーと同じ結果*1を得られます。 パッと見で何をしたいのかがわかりやすく、クエリー負荷も減らせます。

SELECT
    tag_name,
    count
FROM
    sample_tag_count
WHERE
    user_id = 'test_user_1'
ORDER BY
    count DESC;

そして、イベントトリガを機能を活用すると、以下の操作を自動で実行してくれます。

  1. タグテーブルに追加、更新したときに、「タグとタグ個数」テーブルを更新する
  2. タグテーブルから削除したときに、「タグとタグ個数」テーブルから削除する

実際に使用しているトリガ関数の一例を下記に示します。

追加・更新時のトリガ関数

CREATE OR REPLACE FUNCTION trigger_sample_tag_insert_update ()
    RETURNS TRIGGER
    LANGUAGE plpgsql
    AS $trigger_sample_tag_insert_update$
BEGIN
    IF NEW IS NULL THEN
        RETURN NULL;
    END IF;

    DELETE FROM sample_tag_count WHERE user_id = NEW.user_id;

    INSERT INTO sample_tag_count (user_id, tag_name, count)
    SELECT
        user_id,
        tag_name,
        COUNT(*) AS tag_count
    FROM (
        SELECT
            user_id,
            tag_name_1 AS tag_name
        FROM
            sample_tag
        UNION ALL
        SELECT
            user_id,
            tag_name_2 AS tag_name
        FROM
            sample_tag
        UNION ALL
        SELECT
            user_id,
            tag_name_3 AS tag_name
        FROM
        sample_tag) sample_tag_name
    WHERE
        user_id = NEW.user_id
        AND tag_name <> ''
    GROUP BY
        user_id,
        tag_name;

    RETURN NULL;
END;
$trigger_sample_tag_insert_update$;

追加・更新時のトリガー関数をテーブルに適用する

CREATE TRIGGER trigger_sample_tag_insert_update
    AFTER INSERT OR UPDATE
    ON sample_tag
    FOR EACH ROW
    EXECUTE PROCEDURE trigger_sample_tag_insert_update();

削除時のトリガ関数

レコード削除時の関数を定義する。

CREATE OR REPLACE FUNCTION trigger_sample_tag_delete ()
    RETURNS TRIGGER
    LANGUAGE plpgsql
    AS $trigger_sample_tag_delete$
BEGIN
    IF OLD IS NULL THEN
        RETURN NULL;
    END IF;
        
    DELETE FROM sample_tag_count WHERE user_id = OLD.user_id;

    INSERT INTO sample_tag_count (user_id, tag_name, count)
    SELECT
        user_id,
        tag_name,
        COUNT(*) AS tag_count
    FROM (
        SELECT
            user_id,
            tag_name_1 AS tag_name
        FROM
            sample_tag
        UNION ALL
        SELECT
            user_id,
            tag_name_2 AS tag_name
        FROM
            sample_tag
        UNION ALL
        SELECT
            user_id,
            tag_name_3 AS tag_name
        FROM
            sample_tag) sample_tag_name
    WHERE
        user_id = OLD.user_id
        AND tag_name <> ''
    GROUP BY
        user_id,
        tag_name;

    RETURN NULL;

END;
$trigger_sample_tag_delete$;

削除時のトリガ関数をテーブルに適用する

CREATE TRIGGER trigger_sample_tag_delete
    AFTER DELETE
    ON sample_tag
    FOR EACH ROW
    EXECUTE PROCEDURE trigger_sample_tag_delete();

おわりに

本記事では、エキサイトブログにおけるDBコスト削減の取り組みと、特にPostgreSQLのイベントトリガ機能を活用した活用例を紹介しました。 イベントトリガを活用すると、DB内であるテーブルのイベントを元に別テーブルの更新ができるようになります。 一方で、トリガ関数の書き方は、見慣れないものであり、複雑なものにするとメンテナンスがしづらいといった問題もあるかと思います。 トリガ関数は適切な場面で活用していきたいと考えています。 エキサイトブログでは、引き続き既存機能の改修や新機能の開発などに取り組んでいきます。

採用アナウンス

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しています。 また、長期インターンも歓迎しています。

カジュアル面談からもOKです。少しでもご興味がございましたら、お気軽にご連絡頂ければ幸いです。

▼ 募集職種一覧 ▼ recruit.jobcan.jp

*1:タグ個数が同じ場合、異なる結果の場合があります。

Spring Securityで、Controller以外で現在ログイン中のユーザ情報を取得する方法

こんにちは。 エキサイト株式会社の三浦です。

Spring Securityを使えば、Spring Bootで簡単にログイン機構を作ることができます。

当然ログイン中のユーザ情報も取得することができます。

Controllerのメソッドの引数から取得する方法が一般的な方法の1つかと思うのですが、実はもっと簡単な方法があります。

import org.springframework.security.core.context.SecurityContextHolder;

public class Sample {
    public User getLoggedInUser() {
        // これだけでユーザ名を取得可能!
        final String name = SecurityContextHolder.getContext().getAuthentication().getName();

        // DB等からユーザ情報を取得する処理
        return getUser(name);
    }
}

上記のように

SecurityContextHolder.getContext().getAuthentication().getName();

を使えば、簡単に現在ログイン中のユーザ名を取得することができます。

(なお、本処理はWebMVCで実行しています。WebFluxの場合は処理が違うようなのでご注意ください。)

Controllerの引数から取得すると、他のクラスやメソッドに引き回す手間が掛かってしまいますが、この方法であればどの場所からもログインユーザ情報を取得できるのでとても便利です。

Spring Securityには色々便利な機能が存在するので、ぜひ使っていきましょう!

MySQLで複合indexのexplainを見る時、key_lenも確認する必要があった話

はじめに

こんにちは。新卒1年目の岡崎です。

MySQLで複合indexを貼るか、単体のindexを貼るのか業務で検討しました。この時は普段確認していること以外に、key_lenも確認する必要があったので、その備忘録として紹介します。また、今回の記事は、初学者向けの内容になっております。

環境

MySQLの5.7です。

事前準備

今回は例として、小説の管理システムを考えます。

テーブル定義は以下のようにしました。

explain

ここで、お知らせの一覧を取得することを考えます。ただし、公開開始日時の降順で取得することとします。

この時のSQLの例は以下です。

SELECT 
    novel_id,
    title,
    author,
    publish_start_at
    
FROM
    novel
    
WHERE 
    publish_start_at <= NOW()
    AND publish_end_at >= NOW()

ORDER BY
    publish_start_at DESC

LIMIT 0, 20
;

この時のexplainを見てみます。

| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra                       |
|----|-------------|-------|------------|------|---------------|-----|---------|-----|------|----------|-----------------------------|
|  1 | SIMPLE      | novel |            | ALL  |               |     |         |     |   30 |    11.11 | Using where; Using filesort |

このexplainの結果を見ると、typeにALL、Extraにfilesortが出ているので、改善の必要があります。

なぜこのような結果になっているのか考えてみます。今回の場合、novel_idがPKとなっています。しかしこのPKを使わずに公開開始日時で並び替えをしているので、このような結果になっている可能性が高いです。したがって、公開開始日時にindexを貼るか、公開開始日時を含む複合indexを貼る必要があります。

where句、order by句に関わるカラムを使ったindexで実験をしてみようと思います。 今回の候補は以下の3つとします。

  1. 公開開始日時のindex
  2. 公開開始日時・公開終了日時の複合index
  3. 公開終了日時・公開開始日時の複合index

それでは、それぞれのexplainを見ていきます。

1. 公開開始日時のindexを貼った場合のexplain

| id | select_type | table | partitions | type  | possible_keys          | key                    | key_len | ref | rows | filtered | Extra                              |
|----|-------------|-------|------------|-------|------------------------|------------------------|---------|-----|------|----------|------------------------------------|
|  1 | SIMPLE      | novel |            | range | PUBLISH_START_AT_INDEX | PUBLISH_START_AT_INDEX | 6       |     |   11 |    33.33 | Using index condition; Using where |

2. 公開開始日時・公開終了日時の複合indexを貼った場合のexplain

| id | select_type | table | partitions | type  | possible_keys                         | key                                   | key_len | ref | rows | filtered | Extra                 |
|----|-------------|-------|------------|-------|---------------------------------------|---------------------------------------|---------|-----|------|----------|-----------------------|
|  1 | SIMPLE      | novel |            | range | PUBLISH_START_AT_PUBLISH_END_AT_INDEX | PUBLISH_START_AT_PUBLISH_END_AT_INDEX | 6       |     |   11 |    33.33 | Using index condition |

3. 公開終了日時・公開開始日時の複合indexを貼った場合のexplain

| id | select_type | table | partitions | type  | possible_keys                         | key                                   | key_len | ref | rows | filtered | Extra                                 |
|----|-------------|-------|------------|-------|---------------------------------------|---------------------------------------|---------|-----|------|----------|---------------------------------------|
|  1 | SIMPLE      | novel |            | range | PUBLISH_END_AT_PUBLISH_START_AT_INDEX | PUBLISH_END_AT_PUBLISH_START_AT_INDEX | 12      |     |   20 |    33.33 | Using index condition; Using filesort |

typeを見ると、1、2、3の全てがALLではなく、rangeになりました。しかしExtraを見ると、3だけはfile sortが残ってしまっています。よって3は、1と2と比べてパフォーマンスが劣りそうなことが分かります。

それでは、1と2のどちらがいいでしょうか。

key_len

この時見なくてはいけないのは、key_lenです。

key_lenは何であるのか公式ドキュメントを見てみます。

key_len カラムは、MySQL が使用することを決定したキーの長さを示します。 key_len の値を使用すると、MySQL が実際に使用するマルチパーティキーの部分の数を決定できます。 key カラムに NULL と表示されている場合、key_len カラムにも NULL と表示されます。

つまり、key_lenとは選択されたindexのキーの長さになります。例えばdatetimeなら5〜8バイトとなります。もっと詳しく知りたい人はここを見てください。

以上のことを踏まえて、2の公開開始日時・公開終了日時の複合indexを見てみます。公開開始日時と公開終了日時の複合indexが効いていた場合、10〜16になるはずです。

しかし、結果は6です。これは公開終了日時の方のindexは使われていないことを示しています。したがって、今回は1の公開開始日時のindexのみで十分なことが分かりました。

最後に

複合indexを貼った時はtypeにALLがないか、Extraにfile sortがないか等、普段気をつけていること以外にもkey_lenを確認してみてください。

ここまで読んでいただきありがとうございました。

【Flutter】Androidの課金APIを操作する前に利用可能判定を行う【in_app_purchase】

エキサイト株式会社の@mthiroshiです。

Flutterのアプリ内課金の実装には、 in_app_purchase パッケージを使います。 Androidで実装する際に、少し躓いた問題があったのでご紹介します。

動作環境は、下記のpubspec.ymlの内容です。

dependencies:
  in_app_purchase: ^3.1.10
  in_app_purchase_android: ^0.3.0+13

購入商品の問い合わせに失敗するケース

アプリ起動時に課金状態を確認するため、購入商品の問い合わせ処理を実装していました。

下記にサンプルコードを示します。

/// アプリケーション用の課金操作クラス
class BillingAndroid {
  BillingAndroid() {

    InAppPurchaseAndroidPlatform.registerPlatform();

    subscription = inAppPurchase.purchaseStream.listen(
      // 購入イベントリスナー
      _listenToPurchaseUpdated,
      onDone: () {
        subscription.cancel();
      },
      onError: (_) {
      // エラー処理
      },
    );
  }

  final InAppPurchase inAppPurchase = InAppPurchase.instance;

  final BillingClientManager clientManager = BillingClientManager();

  late StreamSubscription<List<PurchaseDetails>> subscription;

  /// 購入商品の問い合わせ
  @override
  Future<void> query() async {
    final purchases = await clientManager.runWithClient((client) async {
      return await client.queryPurchases(ProductType.subs);
    });

    if (purchases.purchasesList.isEmpty) {
      // 購入商品がない場合の処理
      return;
    }

    for (final element in purchases.purchasesList) {
      // アプリケーションのAPIでレシート検証を行う
    }
  }

処理の流れは、下記になります。

  • InAppPurchaseクラスの初期化
  • Androidの課金クライアントを管理するBillingClientManagerqueryPurchasesを実行して、購入商品データ(レシート)を取得
  • 購入商品データの検証

BillingAndroidクラスのコンストラクタで、InAppPurchaseの初期化と購入イベントのリスナー設定を行っています。 そして、アプリ起動時に query() を呼び出して、購入商品の問い合わせを行います。

上記の処理では、商品を購入した状態にも関わらず、稀に空のレスポンスを返す挙動が起きていました。

課金APIの利用可能状態を確認する

InAppPurchase クラスには、 課金APIの利用可能状態を取得する isAvailable() があります。

pub.dev

isAvailable()queryPurchases() の挙動を確認してみたところ、 isAvailable() の返り値がfalseのときに、queryPurchases() が空のレスポンスを返していました。

つまり、queryPurchases() が空を返していた理由は、課金APIが利用できない状態だったからでした。

そこで、queryPurchases() の実行前に isAvailable() によって課金APIの利用可能判定を行うことで、この問題を回避しました。

下記に、利用可能判定を入れたコードを示します。

  static const maxRetryQuery = 5;

  int retryQueryCount = 0;

  @override
  Future<void> query() async {
    if (!await inAppPurchase.isAvailable()) {
      // 利用不可の場合はリトライ
      if (retryQueryCount < maxRetryQuery) {
        await Future<void>.delayed(const Duration(seconds: 1));
        await query();
        retryQueryCount++;
        return;
      }
      retryQueryCount = 0;
      return;
    }
    retryQueryCount = 0;

    final purchases = await clientManager.runWithClient((client) async {
      return await client.queryPurchases(ProductType.subs);
    });

    if (purchases.purchasesList.isEmpty) {
      // 購入情報がない場合の処理
      return;
    }

    for (final element in purchases.purchasesList) {
      // APIでレシート検証を行う
    }
  }

isAvailable() がfalseを返す際には、遅延処理を入れてリトライしています。 これにより、課金APIが利用可能な状態で操作できまして、購入商品を正しく問い合せることができました。

公式のサンプルコード

公式のサンプルコードを確認してみると、ストアの初期化処理で isAvailable() から利用可能判定を行っていました。

  Future<void> initStoreInfo() async {
    final bool isAvailable = await _inAppPurchase.isAvailable();
    if (!isAvailable) {
      setState(() {
        _isAvailable = isAvailable;
        _products = <ProductDetails>[];
        _purchases = <PurchaseDetails>[];
        _notFoundIds = <String>[];
        _consumables = <String>[];
        _purchasePending = false;
        _loading = false;
      });
      return;
    }

〜省略〜

github.com

今回対応した処理は、公式のサンプルコードから見落としていたようでした。

まとめ

FlutterのAndroidの課金実装において、課金APIを操作する前に利用可能判定を行うことについて説明しました。 課金APIの接続タイミングによっては想定した挙動にならないケースがあるので、 isAvailable() で確認してから行うことを推奨します。

参考になれば幸いです。