古代のボードゲーム『樗蒲(かりうち)』復元。1000年以上前に大ブームを起こしたボドゲとは?

本日はちょっと変わった話題です。平城京の頃に流行ったと考えられる盤上遊戯の復元の話題です。

その名は『樗蒲(かりうち)』。八世紀中ごろには遊ばれており、賭博の対象にもなって大流行して禁止令まで出たという古代のボードゲームです。

産経新聞に記事が出ていますが、こちらは有料記事なので内容を読めない方も多いと思います。私も契約してないので読めません。そこで『樗蒲(かりうち)』というボードゲームの名称から、どんなゲームか調べてみました。

平城京・謎のボードゲーム復元、都人たちの熱狂を今に(有料記事)

https://www.nikkei.com/article/DGXZQOUD122D70S3A510C2000000/

『樗蒲(かりうち)』とは?

さて、ゲームタイトルの『樗蒲(かりうち)』で検索をかけてみるとWikipediaが出てきました。

wikiでは樗蒲(ちょぼ)という中国のダイスゲームと紹介されています。ダイスの代わりに平たい板を使って行うスゴロクのようなゲームです。朝鮮の『ユンノリ』、古代日本の『樗蒲(かりうち)』とも類似性があるとのことで、いきなりニアピン。

IT社会の恩恵を感じつつ読んでみると、記号が書かれた土器が奈良県で出土しており、今でいうゲームのボードだったと推測されているようです。

産経新聞の記事でも奈良文化財研究所の名前があるので、この樗蒲(かりうち)の話で間違いないでしょう。

しかし、詳しいルールはすでに失われてしまっているとのこと。もっと詳しく知りたい……

wiki 樗蒲

https://ja.wikipedia.org/wiki/%E6%A8%97%E8%92%B2

奈良文化財研究所 学術情報リポジトリ

もっと知りたい!ということで、奈良文化財研究所から情報が出ていないか探してみると、これまた『樗蒲(かりうち)』で検索していくと見つかりました。IT社会!

なぶんけんブログにて、樗蒲(かりうち)の復元についてまとめられたPDFが公開されていました。こちらの資料だと土器の写真も掲載されていますし、朝鮮のユンノリを基に推測されたゲームのルールについても記載があります。

円形に並んだ点がスゴロクのマスになっており、特定のマスに止まるとショートカットできるようです。今遊んでも割と楽しそう。

元はショートカットマスが6か所あった6分割タイプだったのが、12世紀にはショートカットマスが4か所に減った4分割タイプが見られるようになり、韓国では現代でも4分割タイプの変形バージョンが『ユンノリ』として伝わっているとのこと。

8世紀中ごろにはすでに遊ばれていた訳ですから、現代からすると1200年ほど前の出来事。当時は賭博としても流行って禁止令が出たそうです。

しかし、この資料は6年ほど前のもの……産経新聞の記事ではさらに進んだ研究内容が書かれているのかも?と思うとまだまだ興味が尽きません。

なぶんけんブログ 古代の盤上遊戯「樗蒲(かりうち)」の復元

https://www.nabunken.go.jp/nabunkenblog/2016/06/kariuchi-hukugen-20160229.html

1

Game Up Your Life.

Skimieアプリをダウンロードして、タスク管理をゲーム化しよう

Skimieは、仕事や勉強にゲーミフィケーションを取り入れ、モチベーションアップをサポートする情報管理サービスです。

新着記事

東京都のQRコード決済で10%還元キャンペーンが3/23(土)で早期終了

東京都のQRコード決済で10%還元キャンペーンが3/23(土)で早期終了のニュースがありました。この記事を書いているのが「3/21」なので、今日を入れて残り3日になります。3/31(日)までの予定だったので、8日早い終了になります。公式サイトhttps://kurashisupport.metro.tokyo.lg.jp/私は「PayPay」「auPAY」「d払い」「楽天Pay」の中で、「PayPay」がほとんどでしたが、どんな利用割合だったかきになりますね。10%のポイント還元でしたけど、利用店舗が多かったので結構利用した感じはありました。このようなキャンペーンを通して、キャッシュレス化がまた加速しますね。

大阪のお土産って何があるのだろう?

3/23と3/24で大阪に行く予定です。そこで、大阪のお土産について、調べてみました。『りくろーおじさんの店』の「チーズケーキ」と『551』の「豚まん」は、マストですね!楽しみです。私の周りからのお勧めりくろーおじさんの店 焼きたてチーズケーキhttp://www.rikuro.co.jp/551の豚まんhttps://www.551horai.co.jp/カールチーズあじhttps://qa.meiji.co.jp/faq/show/4301?category_id=1&site_domain=default辻利抹茶ラスクhttps://www.otabe.jp/view/category/ct223Web検索1/2(じゃらん.net)https://www.jalan.net/news/article/93846/喜ばれる大阪府のお土産19選!地元民「人気ランキング」&編集部おすすめを紹介01位 豚まん【551蓬莱】02位 焼きたてチーズケーキ【りくろーおじさんの店】03位 堂島ロール【モンシェール】04位 じゃがりこ たこ焼き味ソースマヨ風味【カルビー】05位 GRAND Calbee【カルビー】06位 みるく饅頭 月化粧【青木松風庵】07位 みたらし小餅【千鳥屋宗家】08位 茜丸 五色どらやき【茜丸本舗】09位 本千鳥さぶれ【千鳥屋宗家】10位 ひとくち餃子【点天】編集部おすすめお土産情報岩おこし・粟おこし【あみだ池大黒】大阪花ラング【あみだ池大黒】ポテトチップス 関西だししょうゆ【カルビー】手焼・しょう油味たこ焼【たこ昌】ふる里の味 とん蝶【御菓子司 絹笠】ええもんちぃ【五感】本千鳥饅頭【千鳥屋宗家】お好み焼せんべえ【大阪の味本舗】肉桂餅【八百源来弘堂】Web検索1/2(JR東海ツアーズ)https://travel.jr-central.co.jp/plan/area/osaka/omiyage/絶対外さない!大阪の定番お土産6選1.生乳の香りとコクを包み込んだケーキ 「堂島ロール」2.大阪土産の定番中の定番 「551蓬莱 豚まん」3.30年愛される大阪産(もん) 「千鳥屋宗家 みたらし小餅」4.このかたさが癖になる 「あみだ池大黒 岩おこし」5.行列も納得の美味しさ 「りくろーおじさんの店 焼きたてチーズケーキ」6.黒豆入りのマドレーヌ 「ええもんちぃ」ワンランク上の見た目と味!大阪のおしゃれなお土産6選1.プレミアムなポテトチップス 「グランカルビー」2.パティシエが考えた新しい大阪土産 「瓢月堂 たこパティエ」3.お花のカタチのラングドシャ 「あみだ池大黒 大阪花ラング」4.健康や美容を気遣う方へ 「KAGOME GREENS Catch the Rainbow 食べるスムージー」5.日本で作ったフランス伝統のお菓子 「カヌレ堂CANELÉ du JAPON カヌレ」6.大阪発の新しいチーズスイーツ 「ウメダチーズラボ クッキー」

TポイントがVポイントに変わる?

Tポイントを持っている人が多いので、気になっている人は多いのではないですかね?ただ、実際どうなるのか調べていない人が大半なのではないでしょうか。そんな中の一人の私が、調べてみました。参考サイトhttps://web.tsite.jp/vpoint/質問形式いつから変更されるの?→2024年4月22日Tカードは使えなくなるの?→そのまま利用ができる何かしないといけないの?→必要なしVポイントに変わると何がある?→利用できる店舗が増える(すき屋、なか卯、LOTTELIAなど)変更点は?→「Tポイント」アプリをアップデートすると、「VポイントPay」アプリに変更される(予定)感想Tポイントは、今までポイントカードだったが、「VポイントPay」としてQRコード決済できるようになると利用の仕方が変わるかも。後、TポイントからVポイントへの手続きが一番気になるポイントだったが、カードはそのまま、アプリはアップデートする事で、そのまま利用できるのであれば、とてもスムーズで良いと思いました。気になる点としては、TポイントはPayPayなどのアプリで連携しているので、そこは再設定など必要になるかが少し気になりました。

私の近所にある複数のスーパーのメリット・デメリットを書き出してみた

2024年3月12日に書いています。最近、色々なスーパーに足を運ぶことが多いので足で稼いだ情報を整理しています。私の通っているスーパーなので、他の店舗とサービスが違うかもしれません。最初に現状の推しを言っておくと「マルエツ」です。理由は、セルフレジや、Scan&Goなど、スーパーで一番不満になる「待ち時間」について、新しいサービスを早く導入して緩和をしようとしているからです。ただ利用頻度で言うと、「サミット」、「ハナマサ」を多く利用しています。候補5つ①マルエツ②ピーコック③サミット④まいばすけっと⑤ハナマサ①マルエツ【メリット】セルフレジが充実Scan&Goが導入されていて、スマホで商品バーコードをスキャンしてすぐに決済ができるヨーグルトの種類が多く比較的安いTポイントが付く ※Scan&Goのポイントと併用は不可冷凍食品が充実しているパンが充実している【デメリット】2階建て ※生鮮食品などが2階にあり上らなくてはならない買い物カートが利用しにくい ※たくさんの買い物がしにくい②ピーコック【メリット】100円ショップが一緒にあるイオンペイが使える基本は1フロアで欲しい物が揃う ※文房具や日用品は地下時々、アナログの割引券を配っている ※分かりやすく行く動機になる冷凍食品が充実している【デメリット】イオンペイ以外のQRコード決済が使えないセルフレジがないイオン系列の安価な商品が置いていない③サミット【メリット】駐車場がある1フロアで全ての買い物ができる買い物カートを利用しやすい試食をやっていて、かつ気軽に食べやすい季節に合ったサービスを積極的に行っているヨーグルトの種類が多く比較的安いパンが充実している【デメリット】セルフレジがない独自の決済カードのチャージが現金でしかできない④まいばすけっと【メリット】良い意味でコンパクト ※ピンポイントの買い物は利用しやすいイオン系列の安価な商品を購入できるイオンペイが使えるセルフレジがある【デメリット】商品が少ないイオンペイ以外のQRコード決済が使えない⑤ハナマサ【メリット】肉、牛乳、卵、トマトなど、必需品で他に比べて安い商品が多いクレジットのタッチ決済がある冬は比較的高い確率で、みかんの箱売りをやっている【デメリット】店が狭い、階段を下りるのが大変商品カートが使えないセルフレジがない

令和6年4月から"プラスチック"ゴミの出し方が変わる(墨田区版)

墨田区のチラシで、ゴミ捨て方法の変更の案内がきていたので確認してみます。ゴミ捨てについては、市区町村ごとにルールが違います。今回は、墨田区について確認していきます。内容令和6年4月から"プラスチック"ゴミの出し方が変わる詳細素材が全てプラスチックでできている製品を「プラスチック資源」として出す【プラスチックの例】食品トレー"プラマーク"の付いた包装・容器プラスチック製品(概ね30cm以内)【"プラマーク"の付いた包装や容器】※安全でキレイなプラスチック100%素材容器・キャップ類カップ・パック類チューブ類トレー(皿型容器)類食料品や日用品の袋類発砲スチロールなど【プラスチック製品(概ね30cm以内)】バケツ、じょうろストロー、スプーン歯ブラシハンガーなど【プラスチックとして回収できない物】軽くすすいでも汚れが付着しているもの刃物類・発火の危険があるもの在宅医療で使用したものプラスチック以外のものが付着しているものリサイクル整備の故障になるものペットボトル【二重袋に注意!】回収したプラスチックは、全て袋を破いて中身を確認しているため、家庭から出す際は袋などには入れず、そのまま出して欲しいとのこと。確認した感想試みは理解できるが、"プラスチック"の見分け方が複雑に感じるプラスチック資源の分別方法(見分け方)は、対処まで記載されていてGood!個別にルールを決めるのではなく、国で統一できないのだろうか?チラシ(PDF)https://www.city.sumida.lg.jp/kurashi/gomi_recycle/oshirase/pura_start.files/honnkakutirasi.pdf

ナレッジ?ここって何をするんですか?

説明が無いので良く分かんないのです…。 ちなみに画像は私の落描きです適当に貼りました…。

サッカーなどで利用できる都立公園のスポーツ施設がWeb予約可能になったのを知っていますか?

団体でスポーツする際、場所に困っていませんか?私は中野区でサッカーチームに参加しているのですが、場所の確保にはとても苦労しています。区などが管理しているグラウンドは、区に在住か在勤のメンバーを集めての団体登録が必要になるため、チームの中心になっている地域以外の場所を借りるのは、とても難しい状況だと思います。そんな私のような人に朗報で、2024/3/1から都立公園のスポーツ施設がWebから予約可能になりました。都立公園のスポーツ施設は、個人で利用者登録ができ、予約することが可能です。※以前は、施設に行かないと利用者登録ができませんでした、、都立公園スポーツレクリエーション予約システムhttps://kouen.sports.metro.tokyo.lg.jp/web/場所は3つ①代々木公園②高井戸公園③府中の森公園私も3月になって、早速登録して予約をしました。ただ、とても便利になったので、ライバルは多そうです...。なるべく確立の高そうな場所、時間を狙って予約しようと思いますー

「お好み鯛焼き」って知ってますか?

昨日、「お好み鯛焼き」を食べました初めて食べたのだけど、メチャ旨い!また食べたいので、場所をチェック。錦糸町駅のそば、丸井の中にありました。こちらは本店ですね!https://www.0101.co.jp/054/restaurant-menu/omedetaiyaki.htmlつぶあん、カスタードも気になるところ、、

東京都で3/11(月)よりQRコード決済で10%還元のキャンペーンが開始

来週の月曜から、東京都でQRコード決済のポイント還元キャンペーンが始まるので要チェックですね。ポイントを書き出していきます。公式ページhttps://kurashisupport.metro.tokyo.lg.jp/概要キャンペーン期間中に、都内の対象店舗において、対象のQRコード決済を行うと、後日、決済額の最大10%(上限3,000円相当)のポイントを還元!詳細キャンペーン期間2024/3/11(月)~2024/3/31(日) ※早期終了の可能性あり内容対象店舗での決済で10%のポイントが還元されるポイントの付与は後日ポイント付与の上限は、3,000円相当のポイント対象のQRコード(4つ)①PayPay②楽天Pay③au Pay④d払い対象者対象のQRコード決済を行った利用者(都民でなくてもOK)対象店舗キャンペーン開始日から、各QRコード決済のアプリ・HPで確認できるとのこと※以下の対象外を見ると、飲食以外も対象になりそうです!対象外の店舗国、地方公共団体、公共法人が管理運営する施設金融商品取引業者の店舗銀行、信託会社、保険会社等保険医療機関及び保険薬局等風俗営業等の規制及び業務の適正化等に関する法律第2条に該当する施設その他、本事業の目的・趣旨から適切でないと都が判断するもの間違いそうな点対象のQRコード決済ごとに上限3,000円相当のポイントが付与される※4つのQRコード決済を全て利用すると最大12,000円相当のポイントが受け取れる。

GKの"6秒ルール"が変わる?サッカーのルール改善案が下部カテゴリで試験導入の記事

3/4のYahoo記事からピックアップしました。GKの“6秒ルール”は8秒に増加して厳格化、違反時の再開方法も変更へ…IFABが下部カテゴリでの試験導入を承認https://news.yahoo.co.jp/articles/b4bba588cfe02e7dbf5c9261ce2495ec101f1c5dGKの"6秒ルール"とは?GKは、ボールをキャッチしてから6秒以内でボールを手や腕からはなさなければならない変更内容秒数を6秒→8秒に増加する主審は残り5秒から片手を挙げてカウントダウンする再開を「間接FK」→「CK」or「スローイン」に変更する変更の理由ゴール前からの間接FKとする現在の規定が主審に計測の迷いを生じさせているため。感想現在、私は40歳以上(シニア)のカテゴリーでサッカーをやっています。都リーグを始め、区のリーグ、その他の独自リーグなどにも参加しているが、このGKの「6秒ルール」でファールになっているのは見たことがないです。勝つための時間稼ぎは多発するので、導入は進んでいくと思いました。主審のカウントダウンも分かりやすくて良いので、現状のルールでも取り入れて良さそう。秒数は確かにきっちり計測してないので、6秒だと短いのか8秒が適切なのかは気になるところです。

Widgetテストにおける認証系のテスト

Widgetテストでは、エミュレーターや実機を使えないので、Firebaseに問い合わせるユーザー認証の機能が使えません。なので、モックのFirebaseAuthやGoogleSignInのパッケージを使って認証系のテストをします。https://pub.dev/packages/firebase_auth_mockshttps://pub.dev/packages/google_sign_in_mocksテストの流れFirebaseAuthとGoogleSignInのインスタンスのProviderをoverrideして、それぞれのモックのインスタンスに置き換えるAuthenticator のメソッドがモック化されたので、そのメソッドを使ってテストをするpackages/skimie/test/presentation/pages/login_page/components/signup_buttons_test.dartGoogleログインのWidgetテストを解説します。まず始めに、setUpにて、mockUserやmockGoogleSignIn、mockFirebaseAuthのインスタンスを用意します。このときに、mockFirebaseAuthには、mockUserのインスタンスを渡しておきます。 setUp(() async { final MockUser mockUser = MockUser( initialDisplayName: 'skimie_test', ); mockGoogleSignIn = MockGoogleSignIn(); mockFirebaseAuth = MockFirebaseAuth(initCurrentUser: mockUser); await Firebase.initializeApp(); });Widgetテストを開始するときに、対象のProviderをoverrideしてモック化します。ここでは、Googleログインのテストをしているので、そのメソッドがあるGoogleAuthenticatorをモック化しています。 testWidgets('Googleログイン', (WidgetTester tester) async { await tester.pumpWidget( ProviderScope( overrides: [ googleAuthenticatorProvider.overrideWithValue( GoogleAuthenticator( auth: mockFirebaseAuth, googleSignIn: mockGoogleSignIn, ), ), ], child: const MaterialApp( home: Material( child: SignUpButtons(animating: false), ), ), ), );SignUpButtonsをタップしたときには、FirebaseAuthのsignInWithCredentialを使っているので、モッククラス内でこのメソッドを修正します。 @override Future<UserCredential> signInWithCredential( AuthCredential credential, ) async { // ログインしたプラットフォームを確認 Log.i('ログイン : ${credential.signInMethod}'); final userCredential = MockUserCredential(); // currentUserを更新 currentUser = userCredential.user; return userCredential; } setUp内でcurrentUserにMockUserをセットしていましたが、このメソッドを走らせると、別のユーザーにcurrentUserが置き換わる様に修正しました。メソッドが走ったことをユーザー名が変更されていることで確認します。 // signInWithGoogleが呼ばれたことを確認 expect(loggedInCurrentUser.displayName, 'test_login_success');参考:https://qiita.com/mogmet/items/ade07bd842495192922dhttps://github.com/atn832/firebase_auth_mocks/blob/master/test/firebase_auth_mocks_test.darthttps://riverpod.dev/ja/docs/cookbooks/testing#%E3%83%97%E3%83%AD%E3%83%90%E3%82%A4%E3%83%80%E3%81%AE%E6%8C%99%E5%8B%95%E3%82%92%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%A9%E3%82%A4%E3%83%89%E3%81%99%E3%82%8B

インテグレーションテストについて

Flutter公式のドキュメントhttps://docs.flutter.dev/testing/integration-testsはじめに、テスト対象とするOSのバージョンとデバイスを決めてください。デバイスやOSバージョンの普及率などから、テスト範囲を決められるといいと思います。https://developer.apple.com/jp/support/app-store/packages/skimie/integration_test/test_setting.dart上記のファイルのコメントアウトを外すことで、テスト対象とすることができます。iOSの場合XcodeのインストールされているPCで行ってください。テストの流れ利用可能なエミュレータの情報を取得し、Mapに変換するテストに必要なデバイスがインストールされているかをチェックし、なければ追加するエミュレーターを起動し、テストするエミュレーターを終了し、キャッシュを削除する3. に戻り、テスト対象のOSバージョンとデバイスが終わるまで繰り返す1. 利用可能なエミュレータの情報を取得し、Mapに変換するfinal result = await Process.run('xcrun', ['simctl', 'list']);上記のコードで、使用可能なエミュレーターの一覧の文字列が取得できるの、正規表現を使って、Mapに変換する2. テストに必要なデバイスがインストールされているかをチェックし、なければ追加するtest_setting.dart でテスト対象にしているOSバージョンのデバイスが 1. で作成したMapに存在するかをチェックする。ない場合は、インストールする必要があるので、final result = await Process.run( 'xcrun', [ 'simctl', 'create', targetDevice, targetDevice, 'com.apple.CoreSimulator.SimRuntime.iOS-$major-$miner' ], );上記のコードで、追加する。3. エミュレーターを起動し、テストする// エミュレーター起動 await Process.run('xcrun', ['simctl', 'boot', device.id]);上記のコードで、エミュレーターを起動する。// テスト開始 final result = await Process.run( 'flutter', [ 'test', 'integration_test/app_test.dart', '--dart-define=FLAVOR=stg', '--dart-define=INTEGRATION_TEST=true', '-d', device.id, ], );上記のコードで、テストを開始する。packages/skimie/integration_test/app_test.dart に書かれているテストが実行される。4. エミュレーターを終了し、キャッシュを削除する// シミュレーター終了 print('Test Device Shut Down'); await Process.run('xcrun', ['simctl', 'shutdown', device.id]); // キャッシュの削除 await Process.run('xcrun', ['--kill-cache']);上記のコードで、エミュレーターを終了し、念のため、キャッシュを削除しておく。5. 3. に戻り、テスト対象のOSバージョンとデバイスが終わるまで繰り返す3〜5の操作をFor in で回しているので、指定されたテスト範囲が終了するまで、テストを回す。Androidの場合Android Studioをインストールしてある、PCで行ってください。iOSの場合とテストの流れは、大きくは変わりません。テストの流れ利用可能なエミュレータの情報を取得し、Mapに変換するテストに使用するPCのCPUアーキテクチャを確認しますテストに必要なデバイスがインストールされているかをチェックし、なければ追加する必要なシステムイメージがインストールされているかチェックする、なければ追加するエミュレーターを起動し、テストするエミュレーターを終了する。5. に戻り、テスト対象のOSバージョンとデバイスが終わるまで繰り返す1. 利用可能なエミュレータの情報を取得し、Mapに変換する final result = await Process.run('emulator', ['-list-avds']);上記のコードで、使用可能なエミュレーターの一覧の文字列が取得できるの、正規表現を使って、Mapに変換する2. テストに使用するPCのCPUアーキテクチャを確認します// CPUアーキテクチャを判別して、インストールするsystem imageを選択 Future<String> getSystemStringImagesType() async { if (Platform.isMacOS) { final result = await Process.run('sysctl', ['-n', 'machdep.cpu.brand_string']); if ((result.stdout as String).contains('Apple')) { return 'arm64-v8a'; } return 'x86_64'; } return 'x86_64'; }Widndowの場合は、'x86_64'になり、Macは、Apple Silicon(M1など)の場合があるので確認が必要。エミュレーターをインストールするときに指定する必要がある。3. テストに必要なデバイスがインストールされているかをチェックし、なければ追加するテストの対象になっているOSバージョンのエミュレーターがない場合は、インストールする。await Process.run( 'avdmanager', [ 'create', 'avd', '-n', id, '-k', 'system-images;android-$targetApi;google_apis_playstore;$systemImageType', '-d', targetDevice.deviceSetup, ], );上記のコードで、エミュレーターがインストールされる。4. 必要なシステムイメージがインストールされているかチェックする、なければ追加するテストの対象になっているシステムイメージのチェックをするfinal result = await Process.run('sdkmanager', ['--list']);上記のコードで、インストールされているシステムイメージのリストが取得できる。正規表現を使用して、文字列を加工し、テストに必要なシステムイメージがあれば、インストールする // テストに必要なsystem-imageをインストール for (final systemImage in filteredLines) { await Process.run('sdkmanager', [systemImage]); }5. エミュレーターを起動し、テストする// エミュレーター起動 final process = await Process.start( 'emulator', ['-avd', device.id, '-no-snapshot'], );上記のコードで、エミュレーターを起動。// テスト開始 final result = await Process.run( 'flutter', [ 'test', 'integration_test/app_test.dart', '--dart-define=FLAVOR=stg', '--dart-define=INTEGRATION_TEST=true', '--device-id', deviceId ], );上記のコードで、テストを開始する。packages/skimie/integration_test/app_test.dart に書かれているテストが実行される。6. エミュレーターを終了するawait Process.run( 'adb', ['-s', deviceId, 'emu', 'kill'], );上記のコードで、エミュレーターを終了。7. 5. に戻り、テスト対象のOSバージョンとデバイスが終わるまで繰り返す5〜6の操作をFor in で回しているので、指定されたテスト範囲が終了するまで、テストを回す。インテグレーションテストの追加繰り返しているテストは、packages/skimie/integration_test/app_test.dart に書かれているので、テストはこのファイルに追加する。intro_test や email_test の様にそれぞれのテストをディレクトリで分けて、追加していくと、可読性が良いかと思います。import 'package:authenticator/authenticator.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:integration_test/integration_test.dart'; import 'package:skimie/main.dart' as app; import 'login_page/email_login_test.dart' as email_login; import 'intro_page/intro_test.dart' as intro_test; // Test実行 // dart integration_test/test.dart // 実機やエミュレーターでテストする場合は、localeに気をつけてください。 // エミュレーターの場合、'en'になっていることが多いかと思います。 void main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('Integration Test', ( WidgetTester tester, ) async { // アプリ起動 await app.main(); await tester.pumpAndSettle(); // ログアウトしておく final container = ProviderContainer(); final authenticator = container.read(authenticatorProvider); await authenticator.signOut(); await tester.pumpAndSettle(); // イントロ画面の動作確認 await intro_test.test(tester); // Emailログインのテスト await email_login.test(tester); }); }

Firebase Authを用いた認証機能について

FirebaseAuthについては、丁寧なドキュメントがありますので、まずこちらを参考にしてみてください。https://firebase.google.com/docs/auth?hl=jaFlutterでの導入方法も説明されています。https://firebase.google.com/docs/auth/flutter/start?hl=ja各種認証に関しては、Fireabseのドキュメントにコード例も記載してあり、そのコードを利用するだけで、実装可能です。参考:https://zenn.dev/kazutxt/books/flutter_practice_introduction/viewer/30_chapter4_authenticationhttps://zenn.dev/flutteruniv_dev/articles/5f05c75b070b38https://zenn.dev/flutteruniv_dev/articles/25ae47164a1d44

SNS共有機能について

https://zenn.dev/taminaryosuke/articles/fda7ad009a6c69SNS共有機能は、上記の記事を参考にしました。

Flutter おすすめナレッジ

Flutter開発で便利なナレッジを紹介します。カラー色が少ない場合は、Flutterにデフォルトで用意されているClorsクラスから指定すればいいのですが、細かな指定がある場合(カラーコードなど)は、その都度調べて書いていては、間違う可能性もあり、何より面倒です。なので、アプリ全体で色を管理するクラスを始めに作成することが多いです。import 'dart:ui'; class AppColor { const AppColor._(); static const defaultBlack = black900; static const red = Color(0xFFE00000); static const white = Color(0xFFFFFFFF); static const black100 = Color(0xFFE6E6E6); static const black200 = Color(0xFFCCCCCC); static const black300 = Color(0xFFB3B3B3); static const black400 = Color(0xFF999999); static const black500 = Color(0xFF808080); static const black600 = Color(0xFF666666); static const black700 = Color(0xFF4D4D4D); static const black800 = Color(0xFF333333); static const black900 = Color(0xFF1A1A1A); }テキストスタイルフォントの指定や、テキストサイズの指定などは、アプリ内でそれほど多くの種類があるわけではないので、こちらもカラーと同じように、アプリ全体でテキストスタイルを管理するクラスを作成した方が間違うことがなく、統一感も出ると思います。下のように定義しても、適宜、修正することも可能です。import 'package:flutter/material.dart'; import 'app_color.dart'; class AppTextStyle { const AppTextStyle._(); static TextStyle get _base => const TextStyle( color: AppColor.defaultBlack,                 // フォントの指定がある場合は、ここに追加する ); static TextStyle get style8 => const TextStyle( fontSize: 8, height: 16 / 8, fontWeight: FontWeight.w300, ).merge(_base); static TextStyle get style12 => const TextStyle( fontSize: 12, height: 20 / 12, fontWeight: FontWeight.w400, ).merge(_base); static TextStyle get style16 => const TextStyle( fontSize: 16, height: 24 / 16, fontWeight: FontWeight.w400, ).merge(_base); static TextStyle get style20 => const TextStyle( fontSize: 20, height: 28 / 20, fontWeight: FontWeight.w500, ).merge(_base); } Text( num.toString(), style: AppTextStyle.style20.copyWith( color: AppColor.red, ), ),

in_app_purchase を用いたアプリ内課金について

アプリ内課金に使用しているパッケージhttps://pub.dev/packages/in_app_purchase実装例https://github.com/flutter/packages/blob/main/packages/in_app_purchase/in_app_purchase/example/lib/main.dartSkimieでは、課金のメソッドをNotifierProviderで管理しています。アプリ内課金を実装するためには、Google Play Console とApp Store Connect(Apple)の設定が必要です。それぞれのプラットフォームの設定は、Google :https://developer.android.com/google/play/billing/getting-ready?hl=jahttps://www.revenuecat.com/docs/android-productsApple:https://www.revenuecat.com/docs/ios-productsアプリ内課金の流れアプリ内購入が利用可能な状態なのか確認する購入品の製品IDのSetを用意する。それぞれのストアに問い合わせて製品情報を取得する上記で取得した製品情報を使って購入処理を行う購入処理進行を監視して、購入が完了した後にバックエンドでレシートの検証を行う検証が成功した場合、ジェムをユーザーに付与する(課金の対価を付与する)返ってきた検証結果に応じて、通知を行う購入のトランザクションを完了させるNotifierProviderで持つ状態と上記の順番に沿って説明を進めます。全体のコードを確認するには、/skimie/lib/services/app_purchase/app_purchase_provider.dartを参照してください。Stateとしては、下記の状態を定義しました。@freezed class AppPurchaseState with _$AppPurchaseState { const AppPurchaseState._(); const factory AppPurchaseState({ // 課金利用の可否 @Default(false) bool isAvailable, // バックエンドから取得した課金商品情報(productIdや購入品Image) Items? purchaseItems, // AppleやGoogleのプラットフォームから取得した課金商品の情報 @Default([]) List<ProductDetails> products, // 課金処理の状態 @Default(false) bool purchasePending, }) = _AppPurchaseState; ProductDetails? getProductDetail(Item item) { final productId = item.code; return products.firstWhereOrNull((e) => e.id == productId); } }purchasePending 以外は、全てAPIから取得した値を定義しています。getProductDetailsのメソッドは、OpenAPIで定義されたItemの型の中にある製品IDに該当するproductDetailsをリストの中から取得するメソッドです。単回購入処理のときに使います。1. アプリ内購入が利用可能な状態なのか確認する // アプリ内購入が利用できるかを確認 final isAvailable = await _inAppPurchase.isAvailable(); // アプリ内購入が利用できない場合 if (!isAvailable) { Log.e('アプリ内課金が利用できません。'); state = state.copyWith( isAvailable: isAvailable, ); return; }ストアにアクセスできない、デバイスがGoogleにログインしていないなど、利用不可の場合は、この先の処理には進むことはないです。2. 購入品の製品IDのSetを用意する。 // 課金商品の情報を取得 final purchaseItems = await _purchaseService.getProductIds(); final productIds = purchaseItems.items.map((p0) => p0.code).toSet();バックエンドから、購入品のItemsのデータを取得し、製品IDのSetを作成します。3. それぞれのストアに問い合わせて製品情報を取得する // 課金処理に使用する商品一覧を取得 final response = await _inAppPurchase .queryProductDetails({...productIds, 'android.test.purchased'});このAPIは、1. で作成した製品IDに該当する購入品の情報を Google Play Console や App Store Connect から List<ProductDetails> という型で取得することができます。ProductDetailsからは、購入品のid や title、description、price などの情報が取得できます。4. 上記で取得した製品情報を使って購入処理を行うこの処理はUI側で行われる処理です。 // アプリ内購入品のリスト List<Widget> purchaseItemList = []; final purchaseItems = ref.watch(appPurchaseServiceProvider .select((s) => s.purchaseItems?.items.toList())) ?? []; final isAvailable = ref.watch(appPurchaseServiceProvider.select((s) => s.isAvailable)); for (final item in purchaseItems) { final product = ref.watch(appPurchaseServiceProvider).getProductDetail(item); if (product == null) continue; purchaseItemList.add( ActionBtn( label: "", disabled: !isAvailable, onPressed: () async { await ref.read(appPurchaseServiceProvider.notifier).buyProduct( context, product: product, ); }, color: Colors.amber, width: (width - marginValue * 2 - 80) / 3, height: (width - marginValue * 2 - 80) / 3, image: ProductImage( item: item, displayCount: product.title.getNumInTarget, ), ), ); }appPurchaseServiceProviderのStateから、purchaseItemsを取得し、for in で回します。各item に該当するProductDetailsを取得して、notifierのbuyProductに渡します。AppPurchaseServiceクラス内の単回購入のメソッド、 // 単回購入 Future<void> buyProduct( BuildContext context, { required ProductDetails product, }) async { _context = context; state = state.copyWith(purchasePending: true); final purchaseParam = PurchaseParam(productDetails: product); await _inAppPurchase.buyConsumable( purchaseParam: purchaseParam, autoConsume: _kAutoConsume, ); }各プラットフォームのアプリ内購入が実行されます。※ autoConsume について、デフォルトがtrueになっており、消耗型の購入は、購入と同時にトランザクションが閉じられます。https://techblog.booklista.co.jp/entry/2023/09/29/130633#%E8%B3%BC%E5%85%A5%E5%87%A6%E7%90%86%E3%81%AE%E6%9B%B4%E6%96%B0%E3%82%92listen%E3%81%99%E3%82%8B:~:text=%E3%81%99%E3%81%B9%E3%81%8D%E7%82%B9-,Android,-%E3%81%A7%E3%81%AF%E8%B3%BC%E5%85%A5%E3%82%A2%E3%82%A4%E3%83%86%E3%83%A05. 購入処理進行を監視して、購入が完了した後にバックエンドでレシートの検証を行う /// 購入の処理を監視する Future<void> listenPurchaseUpdated() async { final Stream<List<PurchaseDetails>> purchaseUpdated = _inAppPurchase.purchaseStream; _subs = purchaseUpdated.listen( (List<PurchaseDetails> purchaseDetailsList) { // 購入などが行われた場合の処理 _listenToPurchaseUpdated(purchaseDetailsList); }, onError: (Object error) { Log.e('Error: ${error.toString()}'); errorNotification(msg: _l10n.errorPurchase); }, ); }Streamにて、購入が行われるとpurchaseDetailsList が自動で返される処理になります。返されたpurchaseDetailsListを_listenToPurchaseUpdatedに渡して、検証する流れになります。 // 購入処理が行われた場合にそのトランザクションを処理する void _listenToPurchaseUpdated( List<PurchaseDetails> purchaseDetailsList, ) { purchaseDetailsList.forEach( (PurchaseDetails purchaseDetails) async { if (purchaseDetails.status == PurchaseStatus.pending) { state = state.copyWith(purchasePending: true); } else { // 購入処理がエラーの場合 if (purchaseDetails.status == PurchaseStatus.error) { Log.e('Error: ${purchaseDetails.error}'); errorNotification(msg: _l10n.errorPurchase); } // 購入がキャンセルされた場合 if (purchaseDetails.status == PurchaseStatus.canceled) { successNotification(msg: _l10n.canceledPurchase); } // 購入処理が完了、または復元された場合 if (purchaseDetails.status == PurchaseStatus.purchased || purchaseDetails.status == PurchaseStatus.restored) { // レシートの検証をする final receipt = purchaseDetails.verificationData.localVerificationData; final valid = await _purchaseService.postPurchaseReceipt( receipt: base64Encode(utf8.encode(json.encode(receipt))), itemMasterCode: purchaseDetails.productID, ); if (valid) { // レシートの検証が成功時の処理 successNotification(); // 購入完了の処理 if (purchaseDetails.pendingCompletePurchase) { await _inAppPurchase.completePurchase(purchaseDetails); } } else { // レシートの検証が失敗時の処理 errorNotification(msg: _l10n.errorPurchase); } state = state.copyWith(purchasePending: false); } } }, ); }受け取った、purchaseDetailsListは、forEachで回され、個別の状態に応じて処理をする。purchaseDetails.status == PurchaseStatus.purchased購入済みの状態であれば、  // レシートの検証をする   final receipt = purchaseDetails.verificationData.localVerificationData;レシートを取得することができるので、バックエンドに渡して、レシート検証をしてもらう。6. 検証が成功した場合、ジェムをユーザーに付与する(課金の対価を付与する)バックエンドの処理。7. 返ってきた検証結果に応じて、通知を行うレシート検証から返ってきた値(valid)によって、成功、失敗の通知を行う。8. 購入のトランザクションを完了させる // 購入完了の処理 if (purchaseDetails.pendingCompletePurchase) { await _inAppPurchase.completePurchase(purchaseDetails); }トランザクションを完了していないpurchaseDetailsは、完了の処理を行う。バックエンドの処理が失敗した時の対策https://techblog.booklista.co.jp/entry/2023/09/29/130633こちらの記事で紹介されている方法で実装。 // 未完了のレシートを検出して再検証する Future<void> reCheckPendingTransaction() async { // 未完了のレシートを格納するリスト final List<PurchaseDetails> pendingReceiptList = []; // iOSの場合 if (Platform.isIOS) { final paymentQueueWrapper = SKPaymentQueueWrapper(); final transactions = await paymentQueueWrapper.transactions(); for (final transaction in transactions) { // TODO:transactionから未完了のPurchaseDetailsを取得する } } // Androidの場合 if (Platform.isAndroid) { final androidAddition = _inAppPurchase .getPlatformAddition<InAppPurchaseAndroidPlatformAddition>(); // 過去の購入履歴を取得 final response = await androidAddition.queryPastPurchases(); for (final purchaseDetails in response.pastPurchases) { if (purchaseDetails.pendingCompletePurchase) { pendingReceiptList.add(purchaseDetails); } } } // 未完了の購入トランザクションがあったときにはレシートを再検証する if (pendingReceiptList.isNotEmpty) { pendingReceiptList.forEach( (PurchaseDetails purchaseDetails) async { // 購入処理が完了、または復元された場合 if (purchaseDetails.status == PurchaseStatus.purchased || purchaseDetails.status == PurchaseStatus.restored) { // レシートの検証をする final receipt = purchaseDetails.verificationData.localVerificationData; final valid = await _purchaseService.postPurchaseReceipt( receipt: base64Encode(utf8.encode(json.encode(receipt))), itemMasterCode: purchaseDetails.productID, ); if (valid) { // 購入完了の処理 if (purchaseDetails.pendingCompletePurchase) { await _inAppPurchase.completePurchase(purchaseDetails); } } } }, ); } }リチェックの処理で参考になるレポジトリ:https://github.com/erase2004/InstantCoffee/blob/9278973eb13f70e04653076a2cf651dc13b0cce5/lib/helpers/iap_subscription_helper.dart#L33