cached_network_imageの画像が更新されない

`CachedNetworkImage`で表示している画像が思ったように更新されないという問題にハマりました。

# 前提
* `CacheManager`を継承した`CustomCacheManager`を`cacheManager`として指定した
* `CustomCacheManager`は`stalePeriod: const Duration(days: 1)` と指定した

# 予想していた動作
* 画像を始めにロードして、stalePeriodで指定した1日経過したら、新しい画像があればそれが表示される

# 実際の動作
* 新しい画像が表示されない(古い画像のままである)

# 調査
## flutter_cache_manager
キャッシュの機構は`flutter_cache_manager`を使ってようなのでまずそのあたりを調べました。
[公式ドキュメント](https://pub.dev/packages/flutter_cache_manager)読むとこんな記述がありました。
`stalePeriod`自体はキャッシュが利用されたときからの経過時間なので、任意のタイミングでキャッシュを無効にするようなものではなさそうですね。
というわけで、まず`CustomCacheManager`はあまり意味が無いことがわかりました。(キャッシュ容量を削減するために使う感じかも)

```
The files can be removed by the cache manager or by the operating system. By default the files are stored in a cache folder, which is sometimes cleaned for example on Android with an app update.
キャッシュファイルはcache managerかOSによって削除される可能性があります。
デフォルトではキャッシュファイルはキャッシュフォルダーに保存されます。キャッシュフォルダーは時々クリーンされます。例えばAndroidでアプリがアップデートされるとき。

The cache manager uses 2 variables to determine when to delete a file, the maxNrOfCacheObjects and the stalePeriod. The cache knows when files have been used latest. When cleaning the cache (which happens continuously), the cache deletes files when there are too many, ordered by last use, and when files just haven't been used for longer than the stale period.
cache managerは2つの変数を、いつキャッシュファイルを消すか決定するために利用します。
それはmaxNrOfCacheObjectsとstalePeriodです。
キャッシュはファイルが最近いつ利用されたか知っています。
キャッシュをクリアする時(継続的に発生する)、キャッシュはファイル数が多すぎる時に最後に利用された順に削除します、
またstalePeriodより長い間ファイルが利用されない時にもキャッシュはファイルを削除します。
```

またキャッシュの更新タイミングについては以下のような記載がありました。
要するにいつファイルを更新するかはCache-Controlヘッダの指定値に依存するようです。

```
A valid url response should contain a Cache-Control header. More info on the header can be found here, but in summary it says for how long the image can be expected to be up to date. It also contains an 'eTag' which can be used to check (after that time) whether the file did change or if it is actually still valid.
正しいURLレスポンスはCache-Controlヘッダーを返すべきです。ヘッダについてはもっと知りたい人はこちら。
つまりCache-Controlヘッダはどれくらいの間、画像が最新であるか示すものです。
ヘッダはファイルが変更されたか/そのままかどうかを示すeTagも含みます。

When a file is in the cache that is always directly returned when calling getSingleFile or getFileStream. After that the information is check if the file is actually still valid. If the file is outdated according to the Cache-Control headers the manager tries to update the file and store the new one in the cache. When you use getFileStream this updated file will also be returned in the stream.
ファイルがキャッシュに有るとき、それはいつもgetSingleFileかgetFileStreamから直接返されます。
その後、ファイルが最新かチェックされます。
もしファイルがCache-Controlヘッダーから期限切れの場合には、マネージャー(CacheManger?)はファイルを更新してキャッシュに保存しようと試みます。
getFileStreamを使うとき、この更新されたファイルもストリーム内でリターンされます。
```

## 実験
そこで `Cache-Control: max-age=300` として実験しました。
ちなみに元の実装では未指定で、未指定の場合はmax-ageは7日と判断されるようです。中々更新されいないですね。
max-ageを指定した実験では以下のような状況になりました。

* アプリを再起動すれば300秒経過後は新しい画像が読み込まれる
* しかしアプリを再起動しないとpull to refreshで更新されるウィジェットの画像は古い画像のまま

## 調査
色々breakpointを貼りながらためしていたところ、以下のことがわかりました。

`.pub-cache/hosted/pub.dartlang.org/flutter_cache_manager-3.3.0/lib/src/cache_manager.dart` の getFileStreamが呼ばれる時と呼ばれない時がある。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18273/120dd3c9-2b63-c59f-8032-193c690d8b7b.png)

121行目の_pushFileToStreamの中で、キャッシュの期限が切れた場合に更新を試みる処理が含まれているので、getFileStreamが呼ばれないと更新もさないのもわかる気がします。

で、更に調べていったところどうやら以下のような動きをしていることがわかりました。

* 該当のウィジェットがアプリ起動後に初めて生成される場合には、呼ばれる。
* pull to refreshで更新される時は呼ばれない。

ここまで調べたところ、`flutter_cache_manager` 側には特に問題無さそうな気がしました。
そこで`cached_network_image`を調べることにしました。

CachedNetworkImageのbuildではOctoImageウィジェットを返しているのですが、その中で少しきになる記述がありました。

`.pub-cache/hosted/pub.dartlang.org/octo_image-1.0.0+1/lib/src/image/image.dart`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18273/7239c8b3-3d85-acf2-d4ac-78a38a5bbfb6.png)

`didUpdateWidget` 名前からして、ウィジェットが更新されるタイミングになにかするやつ。
そしてその下には、 oldWidget.image != widget.image の比較。
つまりImageProviderの比較関数で同一と判断できるか否かで分岐したりしている。

そしてCachedNetworkImageProviderの実装を見てみると。
このような記述。今回cacheKeyもscaleもmaxWidthもmaxHeightも指定してないので、urlが変わらない場合ImageProvicerは同じと判断されそう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18273/76af2a2a-2cc1-917a-57e1-fb755cddeb48.png)

ここでこんな予想ができました。

* 画像のURLが変わらないと、CachedNetworkImageProviderがウィジェット更新のタイミングで変更なしと判断され、画像を更新する可能性のあるロジックがそもそも実行されないのでは？

で、maxWidthをランダムにして実験してみました。
するとmax-age秒経過後に画像が更新されるようになりました。
この予想はあっていたようです。

cacheKeyをうまいことつかえば、より任意のタイミングで更新できそうですね。 urlにyyyymmddを付与すると、必ず日をまたげば最新になりそう。

# まとめ
* Cache-Controlを設定しよう
* ウィジェットがリビルドされてもURLが一緒だと更新されないよ
* その場合はcacheKeyを指定するのも有用だね(リビルドのタイミングでcacheKeyが変わっていれば画像が更新される)