.NET MAUIでCollectionViewが真っ白になる原因と対処法|FFImageLoading CachedImage・WebViewの安定化

.NET MAUIのニュースアプリでCollectionView一覧を表示していると、しばらくして突然要素が消え、画面が真っ白になる——例外も出ず原因が掴みにくい症状です。FFImageLoadingのCachedImageやWebViewを併用している場合の落とし穴と、安定化までの具体的な手順をまとめます。

目次

症状の特徴:エミュレーターでも実機でも「一覧が消えて空表示」になる

まずこの手の問題は、クラッシュや例外ではなく「描画が成立しない」「Viewが再利用できず崩れる」「メモリ圧迫でOS側が強制的に間引く」など、表面上は正常に動いているように見えるのが厄介です。今回のケースを、現場でよく見る症状に寄せて整理すると次のようになります。

  • ニュース一覧は CollectionView。行ごとに画像+タイトル+説明などを表示している。
  • 画像は FFImageLoading.Maui の ffimageloading:CachedImage で読み込み・キャッシュ・プレースホルダー表示をしている。
  • 記事詳細は WebView でURLを表示する(広告や計測タグ由来のJavaScriptエラーがログに見えることがある)。
  • しばらく操作(スクロール、タップで詳細へ遷移、戻る、タブ切り替え等)を繰り返すと、一覧の要素が突然消えて真っ白(空)になる。
  • アプリは落ちず、例外も目立たない。ログにも決定打がないため、原因が散って見える。

この症状は「画面全体が白い」のでWebViewが原因に見えがちですが、実際には一覧側(CollectionView+画像コントロール)の再利用と描画が崩れているパターンが多いです。特に、非同期で画像を読み込みながらセルがリサイクルされる状況は、UIフレームワークの弱点が出やすい領域です。

原因の当たりを付ける:疑うポイントを「再現条件」と「観測」で絞る

「真っ白」現象は、1つの原因で起きることもあれば、複数の要素が重なって表に出ることもあります。闇雲に設定を変えるより、疑うポイントを並べ、再現条件と観測方法で潰していく方が早く収束します。

疑うポイントよくある兆候確認方法(最小コスト)対処の方向性
ffimageloading:CachedImage の再利用不整合項目数が多い/高速スクロール/画像読み込み中のタップで発生しやすい。GCログが増えることも。CachedImageを標準Imageに置換して再現性が落ちるかを見る。Imageへ置換、画像軽量化、表示中のみ読み込む。
メモリ圧迫(WebView+画像+スクロールの複合)詳細→戻るを繰り返すほど発生率が上がる。端末差がある。Android Studio / Xcodeでメモリ推移を見る。戻るたびに増え続けるか。WebViewを明示的に解放、画像キャッシュやプレースホルダーを見直す。
Binding不一致/ItemsSource更新の破綻一見「空」だが、ItemsSourceがnullや空に差し替わっている場合。Binding診断、ItemsSourceの参照・件数をログに出す。ObservableCollection運用、UIスレッド更新、差し替え頻度を下げる。
Hot Reloadの副作用開発時のみ不安定で、クリーンビルド/ストアビルドでは出にくい。Hot ReloadをOFFにして再現性を見る。一時的に無効化、ビルド構成の切替で検証。
Shellのタブ/Flyout切替でのページ保持ページが破棄されず裏で残る(WebViewなどが温存されやすい)。ページライフサイクル(OnAppearing/OnDisappearing)でログを確認。遷移時の解放、キャッシュ戦略(ページを作り直す)を検討。

今回の整理では、主因はCollectionView内でのCachedImageの挙動(再利用/メモリ圧迫/描画不整合)が濃厚で、特に「画像読み込み中のタイミングでタップ等が絡む+項目数が多い」条件で再現しやすい、という見立てが現実的です。

最優先の回避策:CachedImageを標準のImageに置き換える

原因の確定が難しいときほど、まず当たりの強いコンポーネントを外して安定化させるのが鉄則です。FFImageLoadingのCachedImageは便利ですが、CollectionViewのセル再利用と相性が悪い状況があり、結果として「セルがうまく描画できず空になる」形で表面化することがあります。

まずは、画像を表示している部分を標準のImageに置換して挙動を確認してください。実際、これだけで白画面化が収まるケースがあります。

<!-- NG例(現象の起点になりうる) -->
<!--
<ffimageloading:CachedImage
  Grid.ColumnSpan="2"
  Aspect="{OnIdiom Desktop=AspectFill, Phone=AspectFit}"
  DownsampleToViewSize="True"
  ErrorPlaceholder="no_image.png"
  LoadingPlaceholder="loading.gif"
  Source="{x:Binding UrlToImage, TargetNullValue=no_image.png}" />
-->

<!-- OK例(まずは安定化) -->
<Image
  Grid.ColumnSpan="2"
  Aspect="{OnIdiom Desktop=AspectFill, Phone=AspectFit}"
  Source="{x:Binding UrlToImage, TargetNullValue=no_image.png}" />

「キャッシュが欲しいからCachedImageを使っていた」という場合でも、MAUI標準の UriImageSource で最低限のキャッシュを確保できます。FFImageLoadingほど多機能ではない一方で、フレームワーク標準のため相性問題が起きにくいのが利点です。

<Image Aspect="AspectFill">
  <Image.Source>
    <UriImageSource
      Uri="{Binding UrlToImage}"
      CachingEnabled="True"
      CacheValidity="7.00:00:00" />
  </Image.Source>
</Image>

プレースホルダー表示をどうするかが悩みどころですが、実運用では次のような設計が安定しやすいです。

  • 画像が来るまで 固定のPNG を出す(GIFは端末によって負荷になりやすい)。
  • サムネイル領域に ActivityIndicator を重ねる(軽量)。
  • 「すべての行で同時に画像取得」にならないよう、ページングや遅延読み込みを併用する。

なぜ置換が効くのか:CollectionViewのセル再利用と非同期画像読み込みの衝突

CollectionViewはスクロール性能のために、画面外のセルを破棄せず再利用します。ここに非同期の画像読み込みが絡むと、次のような「ズレ」が起きやすくなります。

  • セルAがアイテム1を表示中に画像読み込み開始 → 直後にスクロールでセルAがアイテム20に再利用 → 遅れて返ってきた画像がアイテム20側に誤って適用される
  • プレースホルダーのアニメーション(GIF等)が想定以上にメモリとGPUを使い、描画が破綻する
  • 画像コントロールが内部で保持している参照が解放されず、スクロール・遷移を繰り返すほどメモリが増える

標準Imageへ置き換えると、こうした「追加機能が生む複雑性」が減り、まず安定します。安定した後で「どこまで機能を戻すか」を検討するのが安全です。

再現しやすい条件と、短時間で再現性を上げるテスト手順

原因を「推測」で終わらせないために、再現条件を固定します。次のチェックを入れると、短時間で再現性が上がりやすいです。

テスト条件狙い具体的なやり方
項目数を増やす(例:100〜300件)セル再利用・画像読み込みの競合を起こしやすくするAPIのページングを広げる/一時的にダミーデータを増やす
高速スクロール+連打タップ画像読み込み中のタップを重ね、タイミング問題を顕在化スクロール→止めずに任意の行をタップ→戻るを繰り返す
詳細WebViewを何度も開閉WebViewのメモリ保持・解放不足を顕在化10〜30回程度、開く→戻るを繰り返してメモリ推移を見る
低〜中スペック端末で確認メモリ上限に早く到達し、白画面化が出やすい古いAndroid端末やメモリの少ないエミュ設定で試す

このテストで「CachedImage→Image置換」だけがはっきり効く場合、切り分けとしてはほぼ勝ちです。逆に置換しても改善しない場合は、WebViewやデータ更新(ItemsSource)側の可能性が上がります。

AndroidのWebViewを安定化する:JavaScriptとMixed Contentの設定

記事詳細をWebViewで表示する場合、ニュースサイトによってはJavaScriptが必須だったり、httpsページ内でhttpリソースを読み込むMixed Contentが混ざっていたりします。これらがブロックされると、ページが途中で止まったり、真っ白に見えたりします。

AndroidでWebViewを使うなら、まずは次の2点を確認します。

  • JavaScriptEnabled を有効にする
  • MixedContentMode を必要に応じて許可する(調査目的なら一旦AlwaysAllowで切り分け)

MAUIではWebViewHandlerのマッピングを追加して、PlatformViewの設定を行う方法が定番です。

// MauiProgram.cs(Android向け設定の例)
using Microsoft.Maui.Handlers;

#if ANDROID
using Android.Webkit;
#endif

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        // ...省略...

#if ANDROID
        WebViewHandler.Mapper.AppendToMapping("MyWebView", (handler, view) =>
        {
            var webView = handler.PlatformView;
            webView.Settings.JavaScriptEnabled = true;

            // Mixed Content を許可(必要なサイトだけ許可する運用が理想)
            webView.Settings.MixedContentMode = MixedContentHandling.AlwaysAllow;

            // 必要ならクライアントを差し替えて挙動を制御
            webView.SetWebViewClient(new WebViewClient());
        });
#endif

        return builder.Build();
    }
}

注意点として、Mixed Contentを無条件に許可するとセキュリティリスクが上がります。運用では次のように段階を踏むと安全です。

  • まずは「表示が真っ白になる原因がMixed Contentか」を切り分けるため、一時的にAlwaysAllowで試す
  • 原因が判明したら、対象ドメインを限定する/可能ならhttpsのみのURLに変える/外部ブラウザで開くなどに寄せる

SSLエラー時の挙動をコントロールする(必要な場合のみ)

広告配信やリダイレクトの過程でSSLエラーに当たることがあります。ログにそれらしいエラーが出る場合は、WebViewClientをカスタムして「キャンセルして自前のエラーページを出す」など、ユーザー体験を崩さない制御も可能です。

#if ANDROID
class SafeWebViewClient : WebViewClient
{
    public override void OnReceivedSslError(WebView? view, SslErrorHandler? handler, SslError? error)
    {
        // 安易な proceed は推奨されません。基本はキャンセルし、案内を出す方が安全です。
        handler?.Cancel();

        if (view != null)
        {
            view.LoadData("<html><body><h3>ページを表示できませんでした</h3><p>通信の安全性を確認できません。</p></body></html>",
                          "text/html",
                          "utf-8");
        }
    }
}
#endif

画面遷移時にWebViewを解放する:メモリ圧迫を避ける定石

WebViewは内部にレンダリングエンジンを抱えており、画像・スクリプト・フォントなど多くのリソースを保持します。Shellのタブ切替などでページが温存される設計だと、戻ってもWebViewが掴んだメモリが残り続けることがあります。

シンプルで効果が出やすい対策は、ページが閉じるタイミングでSourceをnullにして解放を促すことです。

protected override void OnDisappearing()
{
    base.OnDisappearing();

    if (webView != null)
        webView.Source = null;
}

さらに安定化したい場合は、次のような追加策も有効です(全部やる必要はありません)。

  • WebViewのイベント(Navigating/Navigated)を購読している場合は、OnDisappearingで解除する
  • 詳細ページは「戻るたびに作り直す」方針にし、WebViewを使い回さない
  • ニュースサイトの表示が目的なら、アプリ内WebViewにこだわらず システムブラウザで開く(Launcher.OpenAsync)を選択肢に入れる

データ更新側の落とし穴:ItemsSourceの差し替え・通知漏れで「空」に見えるケース

まれに「白くなった」の正体が描画ではなく、ItemsSourceが空になっている(またはUIが更新されていない)ケースもあります。ログにエラーがなくても次のような条件で起きます。

  • 非同期処理の完了タイミングで、ItemsSourceに新しいListを代入している(参照が変わる)
  • ObservableCollectionではなくListを使い、追加しても通知されない
  • バックグラウンドスレッドでコレクションを更新している
  • フィルタや検索の状態が残り、件数0の表示になっている

この方向を疑うときは「画面が白い=ItemsSourceが0件」かどうかを、件数ログで切り分けるのが最短です。

// 例:一覧表示の直前・直後に件数を出す(ViewModel側)
Debug.WriteLine($"Items count: {Items?.Count ?? -1}");

もし件数が0になっているなら、描画問題ではなくデータフロー問題です。逆に件数があるのに表示されないなら、画像コントロールやWebViewによるリソース圧迫、またはレイアウト崩れの疑いが濃くなります。

EmptyViewのLoading表示が中央ではなく上に寄る理由と直し方

CollectionViewのEmptyViewは便利ですが、「中央に出したいのに上に寄る」ことがよくあります。これはEmptyViewTemplate自体の問題というより、EmptyViewが配置される親レイアウトのサイズとアラインメントの影響を受けるためです。

中央寄せを安定させるコツは、EmptyView側でFill(画面いっぱい)を確保した上でCenterすることです。実装の定石は次の形です。

<CollectionView ItemsSource="{Binding Items}">
  <CollectionView.EmptyView>
    <Grid
      HorizontalOptions="FillAndExpand"
      VerticalOptions="FillAndExpand">

      <StackLayout
        HorizontalOptions="CenterAndExpand"
        VerticalOptions="CenterAndExpand"
        Spacing="12">

        <ActivityIndicator IsRunning="True" />
        <Label Text="読み込み中..." />

      </StackLayout>
    </Grid>
  </CollectionView.EmptyView>
</CollectionView>

ポイントは次のとおりです。

  • EmptyViewの外側(Gridなど)に FillAndExpand を付けて、親の高さを使えるようにする
  • 内側(StackLayoutなど)に CenterAndExpand を付けて中央寄せする
  • 画面全体のレイアウト(親Gridの行定義がAutoになっている等)でも上寄せになるので、親のRowDefinitionやCollectionView自体のVerticalOptionsも確認する

現場で効いた「安定化の優先順位」

最後に、今回のような「CollectionView+画像+WebView」で真っ白になる問題に対して、作業の優先順位をまとめます。迷ったらこの順で進めると、最短で安定化しやすいです。

優先度やること狙い期待できる効果
高CachedImage → Imageへ置換再利用不整合とメモリ負荷の要因を一気に外す白画面化の収束、再現率の大幅低下
高WebViewの設定(JS/Mixed Content)を見直す詳細ページの「真っ白」を切り分けるサイト表示の安定、ログノイズの整理
中OnDisappearingでWebView.Source=null遷移を繰り返したときのメモリ滞留を抑える長時間利用での白画面化を予防
中画像を軽量化(サムネURL・サイズ調整・GIF回避)端末差を減らし、メモリ上限に当たりにくくするスクロール・遷移が滑らかになる
低Hot ReloadをOFFにして検証開発時の揺らぎを除外する「本当にバグか/開発環境由来か」を判別

まとめ:まず一覧を安定させてから、機能を戻す

「しばらくすると一覧が真っ白になる」症状は、ユーザーにとっては致命的ですが、原因がログに出にくく、切り分けが難しいのが現実です。だからこそ、

  • 一覧(CollectionView)内のCachedImageを標準Imageに置換して安定化
  • WebViewはAndroidの設定(JS/Mixed Content)を整え、必要ならエラー時の挙動を制御
  • 遷移時にWebViewを解放して、長時間利用でのメモリ圧迫を抑える

という順番で対処すると、最短で再現率を落とせます。安定した後で、UriImageSourceのキャッシュ活用や画像軽量化など、段階的に最適化していくのが安全です。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次