Photoruction工事中!

Photoructionの開発ブログです!

バリューズカードでチームビルディングしてみたら結構良かった

はじめに

先日、buildチームのオフラインイベントがあり、午前中の1時間半を使ってチームビルディングのセッションを担当しました。
普段はリモートで働いていることもあり、しっかり腰を据えて話し合う機会はどうしても少なくなります。
また、最近新メンバーが2名加入したので、「相互理解を深める」良いタイミングだと思い、バリューズカードを用いたチームビルディングを企画しました。

バリューズカードとは

バリューズカードは、 “価値観” をテーマにしたカードゲームです。

公式では「自己理解を深め、他者との相互理解を促すためのカードゲーム」と紹介されており、 89枚のカードそれぞれに異なる価値観が書かれているカードを選んだり手放したりしながら、自分が何を大切にしているのかを自然に言語化できるよう設計されています。

使い方もシンプルで、テーマに合わせてさまざまなワークが組めるため、 個人の振り返りからチームビルディングまで幅広く活用できるのが特徴です。

get.wevox.io

実際の進め方

計2回プレイしましたが、1回目は↓の公式のルール1のやり方で楽しんでみました。

ちなみに9人でプレイしましたが(推奨は4〜6人)、山札がなくなるまで5周は回せて、特に問題もなく楽しめました。 プレイ時間は1時間半ほどでした。

1. カードを1人5枚ずつ配り、残りで山札を作る
2. 自分の番が来たら山札 or 他の人が捨てたカードから1枚引き、手札の中から一番自分の価値観から遠いカードを1枚選んで捨てる
3. 山札がなくなるまで2を繰り返す
4. 最後に手元に残った5枚のカードをすべてオープンにし、メンバーと共有

また、2つのオリジナルルールも加えてみました。

・ 仕事に限らず人生全体で大切にしている価値観を選ぶ
・カードを捨てるときは捨てるカードを宣言し、一言理由を添える

特に後者のルールは、「幸せを捨てます!」「責任は嫌いなので責任感を捨てます!」という普段耳にしないとんでもないセリフが飛び交い、いい感じに場が盛り上がってよかったです。

最後は手元に残った5枚の価値観カードをランキング形式に並び替え、一枚ずつ理由を言いながら発表を行いました。
1年以上同じチームで働いていますが「へ〜そういう価値観だったんだ」と改めてメンバーのことを知ることができ非常に興味深かったです。

2回目はbuildチームとして大切にしていきたい価値観カードを2枚選択して全員で一斉に発表しました。

こちらはプレイ時間15分くらい。

個人価値観と同じカードを選ぶメンバーがいたり、意外なカードを選んだ人に「その視点も大事だな」と気づかされたりと、短時間ながらもチームとしての方向性を言語化する、とても有意義な時間になりました。

プレイ後

みんなで出した価値観カードは写真に収めて、後日公式サイトでPDF化してNotionにまとめて貼り付けました。

これでいつでも振り返ることができますし、新しくメンバーがジョインした際にはこのページを見せることでチームの雰囲気をつかめます。

感想

チームビルディングに最適な選択肢だったと思いました。

チームメンバーにも好評で、「楽しめた」「最初は何が分かるのかなって半信半疑だったけど、終わってみるとすっきりした」という声が上がっていました。

個人的によかったと思うことは2点です。

1.プライベートはオープンにせずしっかり自己開示ができる

相手を理解しようとすると、どうしてもプライベートに踏み込む質問になりがちです。
ただ、職場という環境では、話す側も聞く側も少し構えてしまい、 「これって聞いていいのかな…?」と互いに慎重になってしまう場面もあります。

その点、バリューズカードだと重すぎず、ゲーム感覚で進められて自然体で話すことができました。

2.相手の価値観をリスペクトした上での立ち回りができる

価値観を知ることで相手の解像度があがり、なぜその発言・行動をしているのか背景を想像できるようになりました。
そのおかげで「その考え方があるからあのような行動につながっているんだ」と腑に落ちることも増えたと思います。

そうすると、意見が対立した際も“相手の価値観を踏まえてどう折り合いをつけるか” という視点で話せるようになります。

単なる賛否のぶつかり合いではなく、

「この人はこういう価値観を大事にしているから、こういう言い方がいいかも」

「ここは譲れない軸なんだな」

といった背景が見えるので、コミュニケーションの質がグッと上がると思います。

さいごに

今回のワークを通して、「価値観を共有することは、チームの土台を強くすることにつながる」

と改めて実感しました。

こういった自己開示の場を積み重ねていくことで心理的安全性が高まり、より過ごしやすくスムーズなコミュニケーションのできる環境へと繋がるのではないかと思います。

また、時間が経ってから再びバリューズカードをプレイし、前回から自分やチームの価値観がどう変化しているのかを確かめてみるのも面白そうだと感じました。

株式会社フォトラクションでは一緒に働く仲間を募集しています

www.wantedly.com

ユニットテストで関数の再定義エラーになった話

はじめに

こんにちは!10月に入社しました、Build Webチームの岡本です!

4年ぶりくらいにPHPを用いて開発を行っています。これまでは、フロントエンドをメインにエンジニアをやってきました。

最近、個人的に学びが多いエラーに遭遇できました👏

とある機能の実装をしていたのですが、リリース準備の作業が迫ってきているというところで、ユニットテストでエラーが発生してしまいました。

そこで本記事では、そのエラーについて調査して整理したことを、サンプルコードを用いながらアウトプットしようと思います!

前提条件

使用技術

まずは、本記事で使用している技術を紹介します。

【言語】
- PHP v8系

【フレームワーク】
- Laravel v10系
- PHPUnit v9系

【そのほか】
- Docker

ユニットテストフレームワークは、PHPUnitを使用しています。

やりたいこと

今回エラーが発生した処理でやりたかったことは、「controllerから受け取った値を画面に表示する際に、その値に対応する文字列と組み合わせて表示すること」でした。

サンプルコード

続いて、本記事で使用するコードです。

実際にエラーに遭遇した時のコードと近い状態を再現するためのサンプルコードを用意しました。

実際のコードでは、DBからデータを取得してくるなどの処理もありますが、今回のサンプルコードではそのあたりは省いて簡単なものにしています。

ただし、処理については、前出の「やりたいこと」で示した内容とほぼ同じにしています。

controller

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Services\SampleMenuService;
use Illuminate\Http\Request;

class SampleController extends Controller
{
    private SampleMenuService $sampleMenuService;

    public function __construct(SampleMenuService $sampleMenuService)
    {
        $this->sampleMenuService = $sampleMenuService;
    }

    /**
     * サンプルページを表示する
     */
    public function getSample(Request $request)
    {
        $menuIds = $this->sampleMenuService->getAllMenus();

        return view('sample.index')->with([
            'title' => 'サンプルページ',
            'menus' => $menuIds,
        ]);
    }
}

blade

@extends('layouts.sample')

@section('title', $title)

@php
    use App\Services\SampleMenuService;

        // idに対応する文字列を組み合わせる
    function convertMenuString($id) {
        return '【'.$id.'】'.match($id) {
            SampleMenuService::OMELET_RICE => 'オムライス',
            SampleMenuService::HAMBURGER_STEAK => 'ハンバーグ',
            SampleMenuService::YAKITORI => '焼き鳥',
        };
    }
@endphp

@section('content')
    <h1>{{ $title }}</h1>

    <div class="items-list">
        <h2>メニュー</h2>
        @foreach($menus as $id)
            <div class="item-card">
                <span class="item-name">{{ convertMenuString($id) }}</span>
            </div>
        @endforeach
    </div>
@endsection

テストコード

<?php

declare(strict_types=1);

namespace Tests\Feature\Http\Controllers\SampleController;

use App\Services\SampleMenuService;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;

class GetSampleTest extends TestCase
{
    use WithoutMiddleware;

    protected function setUp(): void
    {
        parent::setUp();
    }

    /**
     * @test
     */
    public function サンプルページに渡されたタイトルが「サンプルページ」であること()
    {
        $response = $this->get(route('get_sample'));
        $response->assertViewHas('title', 'サンプルページ');
    }

    /**
     * @test
     */
    public function サンプルページに渡されたメニューが正しいこと() {
        $menus = [
            SampleMenuService::OMELET_RICE,
            SampleMenuService::HAMBURGER_STEAK,
            SampleMenuService::YAKITORI,
        ];
        $response = $this->get(route('get_sample'));
        $response->assertViewHas('menus', $menus);
    }
}

ブラウザでは以下のような表示になっています。

遭遇したエラー

エラー内容

遭遇したエラーはこちら。

Cannot redeclare 関数名()(previously declared in ...)

すでにある関数の再定義・重複を禁止するエラーです。

しかも、PHPUnit実行時にのみ、発生していました。

ブラウザを用いた動作確認では問題なく動いていました。

上記のサンプルコードでもテストを実行してみましょう。

PHPUnit実行結果(エラー発生)
今回遭遇したエラーと同様のエラーが出ているのがわかります。

この場合、convertMenuString()という関数が再定義されていることがエラーメッセージから読み取れます。

※ちなみに、テストケースが1件しかない場合は、ユニットテストは通りました。

再定義した記憶ないよ…??

しかし、同じ名前で実装されている関数は見当たりません。

エディタの検索を使っても、ない。

そもそもあれば、ブラウザを用いた動作確認でもエラーになっているかと思います。

原因

ここからは、このエラーの原因について調査したことを整理していこうと思います!

複数のテストケースを用意しており、controllerに定義した同じ関数を毎回呼び出します。 すると、毎回同じbladeを呼び出すことになり、bladeに定義した関数がそのたびに実行されることになります。

サンプルコードのPHPUnitテストコードで見てみましょう。

/**
 * @test
 */
public function サンプルページに渡されたタイトルが「サンプルページ」であること()
{
    $response = $this->get(route('get_sample'));
    $response->assertViewHas('title', 'サンプルページ');
}

/**
 * @test
 */
public function サンプルページに渡されたメニューが正しいこと() {
    $menus = [
        SampleMenuService::OMELET_RICE,
        SampleMenuService::HAMBURGER_STEAK,
        SampleMenuService::YAKITORI,
    ];
    $response = $this->get(route('get_sample'));
    $response->assertViewHas('menus', $menus);
}

2つのテストケースがあります。 それぞれでgetを使ってControllerに実装した関数getSampleを呼び出して実行しています。 Controllerで実装した関数getSampleでは、bladeにデータを渡しています。

return view('sample.index')->with([
    'title' => 'サンプルページ',
    'menus' => $menuIds,
]);

この関数が実行されることにより、bladeで定義された関数も実行されます。

さらに、PHPUnitの設定ファイルであるphpunit.xmlを見てみると、以下のような記述が…!

processIsolation="false”

上記の設定により、テスト実行時にキャッシュを利用する設定になっています。

php.iniの方も確認してみると、OPcacheがenableになっていました。 OPcacheは、コンパイルしたコードをキャッシュとして保存しておき、リクエストのたびにパースする処理を省く拡張モジュールです。

つまり1件目のテストケースで関数を実行したことでキャッシュがあるものの、2件目のテストケースで同じbladeが呼び出されます。すると、bladeに定義した関数が再び実行され、「関数が再定義された」という事象が発生してしまったということです。

解決方法

いよいよ、このエラーを解決した方法です…!!

serviceを用意するなど方法は色々あると思いますが、今回は「無名関数」に変更して解決しました。

bladeに実装していた関数の目的は以下の2つです。

  1. 見た目を整えること
    • ユーザーにとってわかりやすい表示にする
  2. bladeの中で同じ処理をしたいところが数箇所あった
    • 何度も同じコードを書かない

色んなところから呼ばれるというよりそのblade内だけで使いたい処理であり、わかりやすい表示にするため・使い勝手を良くするために必要な処理だったため、無名関数に変更するという修正方針をとりました。

早速サンプルコードの方も、無名関数で書き替えてみます。

@php
    use App\Services\SampleMenuService;

    // function convertMenuString($id) {
    //     return '【'.$id.'】'.match($id) {
    //         SampleMenuService::OMELET_RICE => 'オムライス',
    //         SampleMenuService::HAMBURGER_STEAK => 'ハンバーグ',
    //         SampleMenuService::YAKITORI => '焼き鳥',
    //     };
    // }
    
    // 無名関数に変更
    $convertMenuString = function ($id) {
        return '【'.$id.'】'.match($id) {
            SampleMenuService::OMELET_RICE => 'オムライス',
            SampleMenuService::HAMBURGER_STEAK => 'ハンバーグ',
            SampleMenuService::YAKITORI => '焼き鳥',
        };
    };
@endphp

呼び出しているところも修正します。 変数に代入された無名関数を呼び出すので、$マークがついた$convertMenuStringを呼び出します。

@section('content')
    <h1>{{ $title }}</h1>

    <div class="items-list">
        <h2>メニュー</h2>
        @foreach($menus as $id)
            <div class="item-card">
                    <!-- $マークをつける -->
                <span class="item-name">{{ $convertMenuString($id) }}</span>
            </div>
        @endforeach
    </div>
@endsection

それでは、PHPUnitを実行してみます…!

PHPUnit実行結果(成功)
これで無事、PHPUnitが通りました🎉

ブラウザでの挙動にも影響なしです!

おわりに

以上、PHPUnitで遭遇したエラーと調査したことのアウトプットでした!

エラーが出た当初は、リリース準備のため急ぎで対応する必要があり、「一旦無名関数で対処する」という感覚でした。

しかし、後で原因を調査して整理していると、PHPUnitの設定ファイルの情報に行き着いたり、PHPのErrorに関する情報に行き着いたり、本記事では収まり切らないくらい学びが多い課題でした。

もっと色々と書きたかったですが、テーマの方向性がとっ散らかっちゃうのでここまでとします。

最後までお読みくださり、ありがとうございました!

参考資料

PHP公式ドキュメント 無名関数

PHP公式ドキュメント OPcache

株式会社フォトラクションでは一緒に働く仲間を募集しています

www.wantedly.com

Android Canvas で画像が劣化しない問題を完全解決する ― Bitmap 縮小の最適手法

こんにちは。Androidエンジニアの斎藤です。

はじめに

AndroidCanvas に Bitmap を描画していると、

  • 画像がぼやける
  • 線が潰れる
  • 小さいアイコンが滲む

といった 劣化が発生する問題 に悩むことがありませんか?

特に縮小幅が大きいケースで顕著に劣化します。

フォトラクションでも電子黒板の描画に Canvas を利用しており、この黒板に撮影に関連した画像を描画することがあります。しかしCanvasへの画像を描画するにあたりサイズを縮小すると画像が劣化するケースがありました。

元の画像
Canvasに描画した画像

本記事では、この劣化がなぜ起きるのか、そして Canvas 描画時にも劣化させない最適解を紹介します。

なぜ Canvas に Bitmap を描画すると劣化するのか?

Canvas の描画処理の中で、Android は暗黙的に、Bilinear(バイリニア)補間でスケールしてから描画を行っています。

Bilinear は滑らかに縮小できる反面、以下の欠点があります:

  • 細い線やドット絵がにじむ
  • 極端な縮小で急激に情報が失われる
  • 特に図形、ピクトグラムなどで潰れが出る

Canvas に渡す時点で 元Bitmapを縮小していない と、Canvas 内部の処理(skia)が「雑なスケール」をしてしまい、結果的に劣化します。

Bilinear(バイリニア)補間とは

画像の拡大・縮小は「補間(interpolation)」という処理で行われます。

補間は カーネル(kernel)” と呼ばれる数学的な関数を使って、

新しいピクセル値を、元画像の周囲のピクセルから計算する仕組みです。

カーネル(kernel)とは?

カーネルとは、「どの範囲のピクセルを、どれくらいの重みで参照して新しい値を作るか」を定義する関数のこと。

  • 小さいカーネル → 小さな範囲の平均(計算は速いが品質は低い)
  • 大きいカーネル → 広範囲の情報を使って賢く平均(計算は遅いが品質が高い)

代表的なカーネルを並べると品質と範囲の関係がわかります。

補間方法 カーネルの大きさ 特徴
Nearest Neighbor 1ピクセル “コピー”に近く荒れる。縮小には不向き
Bilinear 2×2 ピクセル 高速だが大幅縮小では情報不足
Bicubic 4×4 ピクセル 滑らかで美しいが計算コストが高い
Lanczos(三次 sinc) 6×6, 8×8… 最強クラスに綺麗だがとても重い

AndroidcreateScaledBitmap(filter = true)が使うのは Bilinear(2×2)です。

解決策:Canvas に渡す前に「質の良い縮小」を行う

Canvas 任せの縮小ではなく、自分で劣化を抑えた縮小処理を行うことが重要です。

ポイントはこの2つ:

  1. 段階的に縮小を行う(一気に1/3以下にしない)
  2. Bilinear 補間を使い、線が潰れないようにする

実際に劣化しない縮小処理の実装(高速 & 高品質)

今回紹介するのは、

  • 高速(最大2回の縮小)
  • ARGB_8888 の画質維持
  • メモリ効率も良い

という ビットマップ縮小アルゴリズム です。

/**
 * 高速かつシャープにBitmapを縮小する
 */
fun resizeBitmapSharpFast(source: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap {
    var currentBitmap = if (source.config != Bitmap.Config.ARGB_8888) {
        source.copy(Bitmap.Config.ARGB_8888, false)
    } else {
        source
    }

    val currentWidth = currentBitmap.width
    val currentHeight = currentBitmap.height

    // まず一気に近似サイズ(ターゲットの2倍)まで縮小
    val approxWidth = targetWidth * 2
    val approxHeight = targetHeight * 2

    val firstStepBitmap = if (currentWidth > approxWidth && currentHeight > approxHeight) {
        val tmp = Bitmap.createScaledBitmap(currentBitmap, approxWidth, approxHeight, true)
        if (currentBitmap != source) currentBitmap.recycle()
        tmp
    } else {
        currentBitmap
    }

    // 最終サイズに縮小
    val finalBitmap = Bitmap.createScaledBitmap(firstStepBitmap, targetWidth, targetHeight, true)
    if (firstStepBitmap != currentBitmap) firstStepBitmap.recycle()
    if (currentBitmap != source) currentBitmap.recycle()

    return finalBitmap
}

Canvas に描画するコード例

val resizedIcon = resizeBitmapSharpFast(originalIcon, width, height)
canvas.drawBitmap(resizedIcon, x.toFloat(), y.toFloat(), null)

結果

線が潰れなくなりました

なぜ段階的縮小は「情報落ち」を抑えられるのか

AndroidCanvas 上で画像を小さく描画すると、細線が滲んだり、潰れてしまったりすることがあります。 これは「縮小時の情報落ち(エイリアシング)」が原因です。

一気に大幅縮小すると起こること:エイリアシング

たとえば “縮小率が大きいまま一気に縮小” すると次の問題が起きます。

  • 細い線(高周波成分)が平均化されて消える
  • ジャギー(ギザギザ)が出る
  • 細線の太さが不均一になる
  • 滲む

これは、画像に含まれる細かい情報(高周波成分)が縮小された先のピクセル数よりも多くなりすぎ、情報が正しく写り取れないためです。

これを エイリアシング(折り返し誤差) と呼びます。

なぜ「段階的縮小」だと改善するのか?

段階ごとに適度なローパス(平滑化)が入り、エイリアシングが起きにくくなるため

具体的には:

  • 小さめの縮小なら、単純な Bilinear 補間でも十分に平滑化を行える
  • 各段階で「細線が消えずに、やや太めに残る」
  • 次の縮小段階でも線が失われずに済む
  • 結果として 最終的な解像度に応じた自然な線の太さとエッジが残る

Canvas の画質劣化は完全に防げる

Canvas 描画時の劣化は Android のスケール処理が原因であり、回避するには次の3つが重要です。

  1. Canvas に渡す前に自前で縮小する
  2. 一気に縮小しない(段階縮小)
  3. 高品質縮小(Bilinear)を使う
株式会社フォトラクションでは一緒に働く仲間を募集しています

www.wantedly.com

Android 16KBページ対応で取り組んだこと

Androidエンジニアの藤井です。

今回は Photoruction Build のAndroidアプリの16KBページ対応について実際に取り組んだ(又は取り組んでいる)ことについて書ける範囲ではありますが、紹介していこうかと思います。

16KBページ対応とは?

16KBページサイズ対応とは、Android OSがメモリを管理する際の最小単位(ページサイズ)を、従来の4KBから16KBに拡大することに伴い、アプリケーション側で必要な互換性の確保と最適化のことです。

📌 なぜ対応が必要なのか?

この変更は、特に大容量のRAMを搭載した現代のデバイスにおいて、パフォーマンスの向上消費電力の削減を目的としています。

ページサイズを大きくすることで、OSがメモリを管理する手間が減り、メモリとストレージ間のデータ転送効率が向上します。これにより、以下のメリットが公式に示されています。

  • アプリ起動時間の短縮 🚀
  • システム起動時間の短縮
  • アプリ起動時の消費電力の削減 🔋

📝 開発者が対応すべきこと

Google Playのポリシーにより、Android 15以降をターゲットとするアプリは、16KBページサイズに対応することが必須となります。

  • Java/Kotlinのみで書かれたアプリは、通常、大きな変更は不要ですが、16KB環境での動作確認が推奨されます。
  • C/C++(ネイティブ)コードネイティブライブラリ.soファイル)を使用しているアプリは、16KBページサイズに対応するようにコンパイルなどの対応が必要です。

詳細については、Android Developersの公式ドキュメントを参照してください。

取り組んだこと

対応要否の調査

まず、当社のアプリでは一部提供されているライブラリにC++製のライブラリが存在しているので、提供元に問い合わせるところから始まりました。

こちらは後に既に問題ないことがわかったので、難なくクリアしました。

ただ、C++製のライブラリ以外でもネイティブライブラリ(.soファイル)を使用している箇所があり、詳細は割愛しますが、それらのライブラリの対応が必要であるとわかるまで調査に時間を費やしました。

最終的には公式で提供されているスクリプトでapkを解析して、使用されているライブラリのELFアライメントヘッダをチェックすることで確認するという情報に辿り着きました。

※ ここまでは筆者の知識不足故にかなり回り道をしてしまった気がしています。

対象となるライブラリの洗い出し

前述のスクリプトを使った調査では実際に下記のような手順で実行しました。

  • 公式で提供されているスクリプト(check_elf_alignment.sh)をアプリのプロジェクトルートに配置
  • ./gradlew assembleProductionRelease でプロダクションビルドのapkを出力
  • check_elf_alignment.sh を使用してapkをチェック

      bash-3.2$ ./check_elf_alignment.sh app/build/outputs/apk/production/release/app-productionRelease_vX.XX.X\(XXX\).apk
    
    • bash で実行する必要があります
  • 出力結果を確認
    • 出力例(抜粋

        Recursively analyzing *****************.apk
      
        NOTICE: Zip alignment check requires build-tools version 35.0.0-rc3 or higher.
          You can install the latest build-tools by running the below command
          and updating your $PATH:
      
            sdkmanager "build-tools;35.0.0-rc3"
      
        === ELF alignment ===
        /var/folders/rm/********/lib/armeabi-v7a/xxxx.so: \e[32mALIGNED\e[0m (2**14)
        /var/folders/rm/********/lib/armeabi-v7a/xxxx-jni.so: \e[31mUNALIGNED\e[0m (2**12)
        /var/folders/rm/********/lib/x86/xxxx.so: \e[32mALIGNED\e[0m (2**14)
        /var/folders/rm/********/lib/x86/xxxx-jni.so: \e[31mUNALIGNED\e[0m (2**12)
        /var/folders/rm/********/lib/arm64-v8a/xxxx.so: \e[32mALIGNED\e[0m (2**14)
        /var/folders/rm/********/lib/arm64-v8a/xxxx-jni.so: \e[31mUNALIGNED\e[0m (2**12)
        /var/folders/rm/********/lib/x86_64/xxxx.so: \e[32mALIGNED\e[0m (2**14)
        /var/folders/rm/********/lib/x86_64/xxxx-jni.so: \e[31mUNALIGNED\e[0m (2**12)
        \e[31mFound XX unaligned libs (only arm64-v8a/x86_64 libs need to be aligned).\e[0m
        =====================
      

ここまでの手順を踏んで得た出力の結果でUNALIGNED と出力されているsoファイルは既に16KBページ対応で対応する必要のあるSDKのsoファイルになります。

これらのsoファイルをALIGNED と判定されるように各種SDKを対応していく必要があります。

GooglePlayに延長リクエス

前述の対応と前後はしますが、調査の過程でGooglePlayに延長リクエストを送ることで元々の期限(2025年11月1日) から2026年5月31日まで延長できることがわかったので、早速リクエストを送りました。

SDKの更新

ここまでの調査で判明したsoファイルから、対象となるSDKを絞り込むことになります。

これはsoファイルの名前から対象となるSDKを推測したり、CursorやChatGPTを始めとするAIサービスを活用して、ファイル毎に調べるなどの手段を用いました。

対応が必要になったSDKやライブラリは大きく分けて下記に分類されます。

  • 有償SDK
  • 公式又はOSSのライブラリ
  • AAR化した独自ライブラリ

殆どのライブラリはバージョンアップによって解決しましたが、AAR化した独自ライブラリに関しては、様々な都合によりソースコードを参照できなかったり、参照できたとしても対応工数が掛かる可能性が高いものでした。

ただ、当社の場合ではありますが、結果的にはAAR化した一部のライブラリの使用を廃止するだけで解決しました。

その結果、現状使用されているSDKの中で対応が必要なネイティブコードのELFヘッダのアライメントは全てALIGNED と判定される状態に持っていくことが出来ました。

今後の取り組み

対応が必要な各種SDKの対応は出来たものの、市場にリリースしているわけではありません。

今後はリリースに向けて影響範囲をテストしていき、必要に応じて段階を分けてリリースしていくことになります。

株式会社フォトラクションでは一緒に働く仲間を募集しています

www.wantedly.com

DroidKaigi2025にゴールドスポンサーとして参加しました

Android エンジニアの斎藤です。

昨年に引き続きDroidKaigiのゴールドスポンサー🥇として参加しました。

昨年参加した記事はこちらです。

本レポートでは、当日の企業ブースの様子や、イベントを通じて得られた学びについてご報告します。

企業ブースについて

今年もゴールドスポンサーとして企業ブースを出展し、我々がどのようなサービスを開発・提供しているかできるだけ多くの方に知っていただくことに注力しました。

去年と同じ位置でブースを開催することができ、これによって認知していただけることもあり運も味方してくれたと思います。

去年に引き続き出展ブースをまわるスタンプラリーのおかげか多くの人に来ていただきました。

セッションの合間など人が多くなる時間には一人一人にサービスを説明することが難しかったですが、多くの方にサービスを知っていただくきっかけを作れたと思います。

ノベルティ 🎁

ブースで配布したトートバッグ、日傘、扇子のノベルティは大変ご好評をいただきました。

前回も好評だったトートバッグですが、今回は前回とは違うデザインのものを用意しました。前回のトートバッグを持ってきてくださる人もいらっしゃって大変嬉しかったです 🙏

そのほか日傘については当日大雨があったこともあり活用いただいたというお声を頂戴しました。(想定外でしたが。)

実は日傘は他のノベルティに比べて在庫数が少なく早い者勝ちとなっていました。

次回出展時も早い者勝ちのノベルティがあるかもしれないのでぜひお早めにブースに来てください 🙇‍♂️

気になったセッション 👀

今回は来てくださる方になるべくサービスの説明することに注力していたので、セッション自体に参加できませんでした。

しかしDroidKaigiでは公式でセッションをアップロードしていただけるので後追いすることができます。大変ありがたいです。

とはいえまだ半分も見れていないのですが、その中で見て興味が湧いたセッションと見れていないけど気になるセッションをあげたいと思います。

見て気になったセッション

EncryptedSharedPreferences が deprecated になっちゃった!どうしよう!

https://2025.droidkaigi.jp/timetable/939336/

フォトラクションでも EncryptedSharedPreferences を使っており対応方法を検討していました。まだ対応はできていないですが、このセッションを見てチームで理解を深めてから対応方針を検討する予定です。

Compose MultiplatformとSwiftUIで作るハイブリッドモバイルアプリ:コード共有とUI融合の実践

https://2025.droidkaigi.jp/timetable/945018/

フォトラクションでは Android, iOS それぞれのチームで開発していますが、KMP、CMPはどちらも興味がある領域なので面白いセッションでした。新規でアプリを作成することがあったら積極的に試してみたいと思いました。

まだ見れてないけど気になるセッション

Gemini エージェントで Android Studio 開発を高速化

https://2025.droidkaigi.jp/timetable/944126/

フォトラクションではAIの活用による開発を推進しています。現在 **GitHub Copilot / Devin / Cursor / Gemini** を使っており、Androidチームでの利用頻度が一番高いのが Cursor ですが、AndroidStudioと Cursor を行ったり来たりしているので、Android Studioで完結できるとなればかなり魅力的です。このセッションを見て開発生産性向上のヒントを得ようと思います。

参加を終えて 😌

DroidKaigi 2025も弊社のサービス「Photoruction」を多くのAndroidエンジニアの方々に知っていただくための、非常に貴重な機会となりました。

今回はどのようなサービス・アプリかをPRすることに注力したこともあって、ブースでの技術的な会話が去年より少し減ってしまい少し寂しかった気もしますが、来場される方のお声を聞くに昨年より認知度が上がってきたと感じます。

来年はもう少しバランスを見ながら技術の交流を楽しめたらと思います。

ブースに来場してくださった方、そしてDroidKaigi運営の皆様今年もありがとうございました 🙇‍♂️

株式会社フォトラクションでは一緒に働く仲間を募集しています

www.wantedly.com

リモートチームで初めてチーム内のオフラインイベントを企画した話

Androidエンジニアの藤井です。

普段はリモートでチーム開発が成り立っている私たちですが、ある日のメンバーとの会話で「オフラインイベントやってるチームも居るし、自分たちが開催するのもアリだよね」というきっかけで、開発案件の振り返りを目的としたオフラインイベントを企画することになりました。

今回は、イベントの様子と、そこで得られた学びについてレポートします。

ボードゲームでアイスブレイク

イベントは、Androidチームのメンバー3名と、ゲストとして参加してくれたiOSチームのメンバー1名の計4名でスタートしました。

冒頭のアイスブレイクでは、みんなでボードゲーム(※)をプレイしました。普段は画面越しにしか見ることのないメンバーの表情を間近で見ることができ、和やかな雰囲気で本題に入ることができました。

※ プレイしたゲームは「現場は安全っていったじゃないですか!〜仕事猫&電話猫カードゲーム〜」というタイトルです。

あの「仕事猫&電話猫」がカードゲームに! 「ヨシ!」を集めて安全確保を目指せ | ねとらぼ

開発案件の振り返りを通じて見えた課題と学び

アイスブレイク後は、イベントのメインである開発案件の振り返りを行いました。今回は特に規模の大きかった案件を対象に、リリースまでのプロセスを徹底的に分析しました。

具体的には、以下の項目を洗い出し、議論を重ねました。

  • 当初の見積もりで起票したチケット
  • 見積もり後に発生・追加されたチケット(バグ、リグレッションテストなど)
  • リリースまでのスケジュールで起きた具体的な出来事

各メンバーが開発中に感じていた所感を率直に話し合うことで、リモートでは気づきにくかった細かなニュアンスや、共通の課題が見えてきました。

特に、見積もりと実際の開発の間に生じるギャップを深く掘り下げたことで、「開発案件に取り組む上での見積もりに対する一つの共通の物差し」を得ることができたのが大きな収穫でした。

当日の振り返りのSlackでのやり取り。JIRAのフィルターからCSVを書き出してストーリーポイントの計算を行っている様子。

チームワークをさらに高めるためのNEXTアクション

今回の振り返りを通じて、チームとしての今後の取り組みをいくつか決定しました。この学びを活かし、今後の開発プロセスをよりスムーズで効率的なものにしていきたいと考えています。

また、今回はAndroidチームに閉じず、他部署からも見えるようにオフィスのパブリックスペースで開催したことで、iOSチームのメンバーにも参加してもらい、部門を越えた知見の共有ができたことも非常に有意義でした。

イベント後には、Androidチームと他部署のメンバー数名で懇親会も行い、さらに交流を深めることができました。

懇親会にて(こんな写真しか撮れておらず…

まとめ

フルリモート体制のチームにとって、対面で集まる機会は貴重です。

今回のイベントは、チームメンバーの協力のおかげで無事に終えることができ、今後の開発に対する確かな手応えを感じることができました。普段はリモートで完結しているチームですが、たまに顔を合わせて話すのもとても楽しいですね。

今後も、リモートと対面それぞれの良さを活かし、より良いチーム作りを目指していきます!

株式会社フォトラクションでは一緒に働く仲間を募集しています

www.wantedly.com

ローカルDBを Realm から Room へ移行します 🔜

Androidエンジニアの斎藤です 👨‍💻

まずはじめにタイトルを見て「Realm を? 2025年に?」と思った方、私も思います。

とはいえ私自身 Realm を触ったのがこの会社が初めてだったのですが Realm の第一印象は特に悪くなく、Roomが出る前の SQLiteDatabase に比べたら選択肢として間違ってなさそうな気もします。

しかし Realm 特色などを理解せずに利用することで予期せぬバグを生んでしまうこともわかりました。

そんな困りごととどのように解決したか(まだしていない)を共有できればと思います。

はじめに

今や Realm を使っているプロダクトはAndroidでは珍しいかと思います。現在でも大半は Realm を使っており一部新しい機能は Room で実装されています。

フォトラクションでは2017年から開発されモバイルアプリとしては歴史が長く、それに加えてさまざまな理由から未だにRealmは残り続けています。

しかし、とある問題が発覚してからようやく重い腰を上げて全てRoomに置き換えようということになりました。

今回はそのとある問題について紹介しRealmをより深く掘り下げていければと思います。

前提条件として既存アプリの現状の設計は過去に公開した記事の通りになっています。

(一部異なるところもありますが大体はこのようになっています)

https://kojichu.photoruction.com/entry/adcale20221209

https://kojichu.photoruction.com/entry/adcale20211215

とある問題について

まずは発生した現象から。

ある Realm からデータを取得する時に、古いデータが取得されることがありました。

しばらく原因不明でしたが、根気よく調査してみると Realm の読み込みはスナップショットベースであることがわかりました。そしてデータ読み込み時に明示的に最新のデータを取得する場合は realm.refresh() を行うことが必要だと。

しかし、そもそもスナップショットベースってなんでしょうか。Realmは他のDBと何が違うのでしょうか。

Realmの設計哲学

Realmは内部的に MVCC(Multi-Version Concurrency Control) を採用しています。

これは一言で言うと:

  • Realmインスタンスは「ある時点のDB状態(スナップショット)」を表す
  • 書き込みは新しいバージョンを作り、読み込みは各スレッドのスナップショットを使ってロックなしで並行処理可能
  • Realmインスタンスはスレッドごとに独立し、最新の状態を使いたければ明示的にrefresh()する必要がある

つまりRealmは「時点固定の読み込みと書き込み分離」が原則で、データは常に「瞬間的な静止画」を見ているイメージです。

ということは refresh を呼ぶだけでいいのですが、これを実装するにあたって更なる疑問が出てきました。

一方でアーキテクチャの常識:関心分離と状態隠蔽

フォトラクションでは記事にある通りRepository層を設けて関心分離と状態隠蔽を行っています。

元となったクリーンアーキテクチャやDDDの考え方では、

  • Repositoryはデータソース(DB)からのデータ取得を抽象化し、変更の検知や状態管理は上位層に任せる
  • Repository内部の状態(キャッシュやタイミング)を外に漏らさず、いつでも「最新の状態を返す」ことが理想
  • さらに複数Repositoryの連携でトランザクションや状態整合性を保ちたい

このため、Repositoryは単純な「CRUD」以上の責務を持たず、状態更新の管理はControllerやUseCaseに委ねることが多いです。

フォトラクションでも「CRUD」以上の責務は ViewModel に委ねています。

しかし、この最新状態保証とRealmのスナップショット設計の相性が悪いのです。

衝突点:Realmのスナップショット設計は「最新状態保証」と相性が悪い

Realmは「スナップショット=状態固定」なので、

  • Repository単位で呼び出されると、その時点の状態を返すだけになりやすい
  • 他スレッドで更新されても、そのRepositoryのRealmインスタンスは更新されない(refresh()しない限り)
  • Repository単体では「最新状態が保証されているか」がわからず、どこでrefresh()すべきかの判断ができない

つまり、

RealmのMVCCは「明示的な更新管理」が必須である一方、Repositoryはそれを隠蔽・分離したいというアーキテクチャの要求と相反する

という構造的な問題を生みます。

今回のケースでは書き込みによって新しいバージョンを作成したが、読み込みは古いバージョンのデータを見ていた。そしてこれを解決するには明示的に読み込み時に refresh() することです。

では RealmObject を取得する Repository クラスの関数全てに refresh() を記述すればいいのでしょうか。

Realmのrefresh()問題とそのコスト

refresh()Realmインスタンスのスナップショットを最新に切り替える処理です。

  • これを呼ばないと他スレッドの変更は反映されない
  • しかし頻繁に呼ぶとパフォーマンスに影響する可能性がある(特に大量の更新がある場合)

このため、

  • 「どこで」「どのタイミングで」「誰が」refresh()を呼ぶかはアプリ設計の重要課題
  • Repository単位で呼ぶと責務が混ざり、呼ばないと最新を見られず、どちらもデメリット大

一般に Realm における refresh() のコストは小さいですが、以下の条件によって変動します:

コスト要因 説明
差分の大きさ 書き込み後の変更量が大きいほど、バージョン間の更新にかかる処理は重くなる
保持中の RealmObject の数 オブジェクトが多いほど、参照整合性のための更新コストが発生
古い Realm インスタンスを長く保持している場合 トランザクションログのサイズが大きくなり、GCや refresh の負荷が上がる

フォトラクションの場合、差分同期という仕組みを利用していますが、建設BPOサービスも行っておりこれによって大量の納品データが格納させると突然巨大なデータが降ってくることがあります。

この時 refresh() のコストが著しく高くなる可能性があります。

以上が発生した問題です。

対策の方向性

  1. 上位レイヤーでrefreshを管理する

    UseCaseやViewModel層でrefresh()を一括管理し、Repositoryは純粋にデータアクセスのみを担当する。

  2. リアクティブ設計を採用する

    RealmのasFlow()や通知機能を使い、データの変化を監視して自動で最新化を反映する。

    → これで明示的refreshが不要になり、MVCCの設計を活かせる。

  3. RealmProviderやラッパーを設ける

    Realmインスタンス生成とrefresh管理を一括する抽象レイヤーを設けて、複数Repositoryでの状態管理を一元化する。

我々の決断としては 4. の Realm を諦める。になりました。

理由としてはどれも修正コストが高く、このコストを払うならRoom移行した方がいいのではという結論でした。

細かいところでは、フォトラクションはリレーションが多いプロダクトなのでSQLテーブル結合を View テーブルで表現した方がシンプルであったり、Realmの部分一致ロジックに不満があったり、あるのですが大きな理由としては今回の問題が一番大きな理由です。

終わりに

Realmは強力で高速なDBであることは間違いないと思います。

しかし、スナップショットベースのMVCC設計は、一般的な関心分離アーキテクチャとぶつかりやすいことを理解しておく必要がありそうです。

今回は Android と Realm について言及しましたが、 他のスナップショットベースのMVCC設計データベースと何かの組み合わせでも起こりうることなので頭の隅に入れておくといいかなと思いました。

株式会社フォトラクションでは一緒に働く仲間を募集しています

www.wantedly.com