FirebaseStorageの画像をシェアしたい
結構躓いたので記事にする
Firebase Storageに画像を保存して、Firestoreに画像のパスを保存して、パスを取得して画像を表示する方法は知っていたが、シェアする機能は作ったことがなかった!
というわけで、今回やってみました!
こちらの完成品を参考に試してみてください。
今回は試しにてやってみて、作った実験用アプリなので深くは追求しないです。
設定
Firebaseの環境構築をして、FirebaseStorageのセキュリティールールはif trueにしておいてください。
imagesフォルダを作成してこちらに画像を保存していきます。
成功すればこんな感じで画像が保存されてます。これだとまだ物足りない気がするので改良が必要と思います。
🎁パッケージを追加する
画像の保存とシェアするパッケージ、ネットワークから、画像のパスを取得するパッケージを追加します。
name: image_share
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: '>=3.1.0 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
cloud_firestore: ^4.9.0
cupertino_icons: ^1.0.2
firebase_core: ^2.15.0
firebase_storage: ^11.2.5
flutter:
sdk: flutter
http: ^1.1.0
image_picker: ^1.0.4
share_plus: ^7.1.0
dev_dependencies:
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
flutter_test:
sdk: flutter
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
モデルクラスを作る。
タイトルと画像のurlだけ今回は定義します。ListViewで表示するときに使います。
class Photo {
final String title;
final String url;
const Photo({required this.title, required this.url});
// from Jsonでデータをシリアライズ(Dartのオブジェクトに変換)する
factory Photo.fromJson(Map<String, dynamic> json) {
return Photo(
title: json['title'] as String,
url: json['url'] as String,
);
}
// toJsonでデータをデシリアライズ(Jsonに変換)する
Map<String, dynamic> toJson() {
return {
'title': title,
'url': url,
};
}
}
画像のアップロード
画像をアップロードするページを作成します。image_pickerを使用して端末の画像にアクセスして、アップロードします。
iOSは、xmlの設定が必要なので追加しておいてください。
ios/Runner/Info.plistのファイルに追加する。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Image Share</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>image_share</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<!-- ここから -->
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires to access your photo library</string>
<key>NSCameraUsageDescription</key>
<string>This app requires to add file to your camera</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app requires to add file to your photo library your microphone</string>
<!-- ここまで -->
</dict>
</plist>
画像をアップロードするページを作成。
import 'dart:io';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image_share/model/photo.dart';
import 'package:image_share/photo_page.dart';
class UploadPage extends StatefulWidget {
const UploadPage({Key? key}) : super(key: key);
State<UploadPage> createState() => _UploadPageState();
}
class _UploadPageState extends State<UploadPage> {
// 画像をFirebase Storageにアップロードする関数
Future<void> fileUpload() async {
try {
final pickedFile =
await ImagePicker().pickImage(source: ImageSource.gallery);
// Null check for pickedFile
if (pickedFile != null) {
final File file = File(pickedFile.path);
final String timestamp = DateTime.now().toString();
// Create storage reference
final storageRef =
FirebaseStorage.instance.ref().child('images/$timestamp');
// Upload the file
final TaskSnapshot uploadTaskSnapshot = await storageRef.putFile(file);
// Once the upload is complete, then get the download URL
final String imageUrl = await uploadTaskSnapshot.ref.getDownloadURL();
// Add to Firestore
final store = FirebaseFirestore.instance;
final photo = Photo(title: timestamp, url: imageUrl);
await store.collection('photos').add(photo.toJson());
} else {
print("No image selected.");
}
} catch (e) {
throw e;
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.red,
title: const Text('Upload Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
onPressed: () async {
await fileUpload();
},
icon: const Icon(Icons.upload)),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => PhotoPage()));
},
child: const Text('Photo Page')),
],
),
),
);
}
}
画像を表示する
次に画像を表示して、シェアボタンを押すと知り合いの人や自分のPCに画像を共有できる機能を作成します。
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:image_share/model/photo.dart';
import 'package:share_plus/share_plus.dart';
import 'package:http/http.dart' as http;
class PhotoPage extends StatefulWidget {
const PhotoPage({Key? key}) : super(key: key);
State<PhotoPage> createState() => _PhotoPageState();
}
class _PhotoPageState extends State<PhotoPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.red,
title: const Text('Photo Page'),
),
// モデルクラスPhotoを使い画像を表示する
body: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('photos').snapshots(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final List<Photo> photos = snapshot.data!.docs
.map(
(doc) => Photo.fromJson(doc.data() as Map<String, dynamic>))
.toList();
return ListView.builder(
itemCount: photos.length,
itemBuilder: (context, index) {
return ListTile(
trailing: IconButton(
onPressed: () async {
try {
final response =
await http.get(Uri.parse(photos[index].url));
print('HTTP Status Code: ${response.statusCode}');
if (response.statusCode != 200) {
print('Failed to get image: ${response.statusCode}');
return;
}
final bytes = response.bodyBytes;
if (bytes.isEmpty) {
print('Empty response');
return;
}
await Share.shareXFiles(
[XFile.fromData(
bytes, name: 'image.jpeg',// 画像の名前
mimeType: 'image/jpeg'// 画像の形式
)],
);
} catch (e) {
print('An error occurred: $e');
}
},
icon: const Icon(Icons.share),
),
title: Text(photos[index].title),
leading: Image.network(photos[index].url),
);
},
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}
}
main.dartでimportして実行する。
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:image_share/firebase_options.dart';
import 'package:image_share/upload_page.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const UploadPage());
}
}
上手くいけば、前回書いた記事のように画像をシェアできるはずです。
まとめ
今回躓いた点は、画像の形式の設定ができてないと、Firestoreから取得した画像のurlにアクセスしてシェアした画像が汎用的なデータとして送られることでしたね。
このように書けばよかったですが、画像はpngだったり様々形式があるので対応が必要なのが課題ですね。
await Share.shareXFiles(
[XFile.fromData(
bytes, name: 'image.jpeg',// 画像の名前
mimeType: 'image/jpeg'// 画像の形式
)],
);
Discussion