Balbas Code

Dartで動的な画像を差分更新する

公開日: 2024-07-24 00:38:00

Dartでの画像の扱いについてはAssets/imagesを作成して画像を表示することをよくやると思います。
しかしこちらでは、動的に画像を更新できないので、リリースしてしまったアプリに対しては画像を更新→審査を通さないといけない。
ということが発生してしまうために、動的に画像を追加することについて調べてみました。

今回のコードでは、実行ボタンを押下したときに以下の処理を行います。
・files.json(マッピングファイル)の画像ファイル名を取得する
・ローカルパスと比較してマッピングファイル名(画像)がなければローカルパスに画像をダウンロード
・ローカルパスにマッピングファイル名(画像)があれば何もしない
・ローカルパスにあってマッピングファイル名(画像)が消された場合、ローカルファイルを削除



下準備

files.json (マッピングファイル)


{
"files": [
"testImage1.png",
"testImage2.png",
"testImage3.png",
"testImage4.png",
"testImage5.png",
"testImage6.png",
"testImage7.png",
"testImage8.png",
"testImage9.png",
"testImage10.png",
"newImage1.png",
"newImage2.png"
]
}

画像を追加したらこちらのマッピングファイルも更新しないといけないとのことです。



サーバー上に画像と同じ階層にマッピングファイルを配置します。
files_json


流れ的にはWebサーバー上に配置した、files.json (マッピングファイル)のURLにアクセス
→JSONでファイル名が取得できる → 取得したファイル名を元にダウンロードや削除の差分更新を行います。




pubspec.yaml


dependencies:
flutter:
sdk: flutter
path_provider: ^2.0.11
http: ^0.13.4




main.dart


import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'dart:convert';

void main() {
// プラグインの初期化
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Image Downloader',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
// ダウンロードした画像ファイルのリスト
List<File> _downloadedImages = [];

@override
void initState() {
super.initState();
// ダウンロードした画像を読み込む
_loadDownloadedImages();
}

// アプリケーションのドキュメントディレクトリ内に "images" サブディレクトリを作成し、そのディレクトリへのパスを返す
Future<Directory> _getImagesDirectory() async {
final directory = await getApplicationDocumentsDirectory();
final imagesDirectory = Directory(path.join(directory.path, 'images'));

if (!await imagesDirectory.exists()) {
await imagesDirectory.create(recursive: true);
}

return imagesDirectory;
}

// "images" サブディレクトリ内のファイルを読み込み、画像ファイルのみをリストに追加する
Future<void> _loadDownloadedImages() async {
final imagesDirectory = await _getImagesDirectory();
final List<FileSystemEntity> files = imagesDirectory.listSync();

setState(() {
_downloadedImages = files
.where((file) => file is File && _isImageFile(file.path))
.map((file) => File(file.path))
.toList();
});
}

// ファイルが画像ファイルかどうかを判定する
bool _isImageFile(String filePath) {
final extension = path.extension(filePath).toLowerCase();
return ['.jpg', '.jpeg', '.png', '.gif'].contains(extension);
}

// サーバーからファイルリストを取得し、ローカルの "images" サブディレクトリに存在しないファイルをダウンロードし、存在しないファイルを削除する
Future<void> syncImagesWithServer(String baseUrl, String jsonFileUrl) async {
try {
// サーバーからファイルリストを取得
final response = await http.get(Uri.parse(jsonFileUrl));
if (response.statusCode == 200) {
final List<dynamic> fileList = json.decode(response.body)['files'];
final imagesDirectory = await _getImagesDirectory();

// ローカルファイルとサーバーのファイルリストを比較
final localFiles = imagesDirectory
.listSync()
.where((file) => file is File && _isImageFile(file.path))
.map((file) => path.basename(file.path))
.toList();

// サーバー上に存在しないローカルファイルを削除
for (String localFile in localFiles) {
if (!fileList.contains(localFile)) {
final filePath = path.join(imagesDirectory.path, localFile);
final file = File(filePath);
if (await file.exists()) {
await file.delete();
print('Deleted local file: $filePath');
}
}
}

// サーバー上の新しいファイルをダウンロード
for (String fileName in fileList) {
final filePath = path.join(imagesDirectory.path, fileName);
final file = File(filePath);
if (!file.existsSync()) {
final fileUrl = '$baseUrl/$fileName';
await downloadAndSaveImage(fileUrl, filePath);
}
}

// ダウンロードされた画像のリストを更新
_loadDownloadedImages();
} else {
print('Failed to load file list');
}
} catch (e) {
print('Error: $e');
}
}

// 画像を指定されたパスに保存する
Future<void> downloadAndSaveImage(String imageUrl, String filePath) async {
try {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode == 200) {
final file = File(filePath);
await file.writeAsBytes(response.bodyBytes);
print('Image saved to $filePath');
} else {
print('Failed to download image');
}
} catch (e) {
print('Error: $e');
}
}

@override
Widget build(BuildContext context) {
// 画像ファイルが保存されているベースURL
final baseUrl = 'https://www.testFolder';
// サーバー上のファイルリストを返すJSONファイルのURL
final jsonFileUrl = 'https://www.testFolder/files.json';

return Scaffold(
appBar: AppBar(
title: Text('Image Downloader'),
),
body: Column(
children: <Widget>[
// サーバーと同期ボタン
ElevatedButton(
onPressed: () => syncImagesWithServer(baseUrl, jsonFileUrl),
child: Text('Sync Images with Server'),
),
// ダウンロードされた画像を表示するリストビュー
Expanded(
child: _downloadedImages.isNotEmpty
? ListView.builder(
itemCount: _downloadedImages.length,
itemBuilder: (context, index) {
return Image.file(_downloadedImages[index]);
},
)
: Center(child: Text('No images downloaded.')),
),
],
),
);
}
}


実行結果
files_json  imageDownload


画像の差分更新とダウンロードした画像を表示できました。


説明


    1.ローカルファイルとサーバーファイルの比較:
        ・syncImagesWithServer メソッドでサーバーからファイルリストを取得し、ローカルディレクトリ内の画像ファイルと比較します。
        ・サーバー上に存在しないローカルファイルを削除します。
        ・サーバー上に存在するがローカルに存在しないファイルをダウンロードします。


    2.ローカルファイルの削除:
        ・syncImagesWithServer メソッド内でサーバー上に存在しないローカルファイルをチェックし、削除します。


    3.新しいファイルのダウンロード:
        ・サーバー上の新しいファイルをダウンロードして保存します。


この修正版コードにより、ローカルの画像パスとサーバー上の画像リストを比較し、必要な場合にのみダウンロードや削除を行うようになります。これにより、無駄なダウンロードやローカルファイルの無駄な保持を防ぐことができます。