Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Firestorage integration #26

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@
"ui": {
"enabled": true
},
"singleProjectMode": true
"singleProjectMode": true,
"storage": {
"port": 9199
}
},
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"storage": {
"rules": "storage.rules"
}
}
40 changes: 40 additions & 0 deletions lib/features/posts/data/firestorage/firestorage_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'package:firebase_storage/firebase_storage.dart';

class FirestorageEventPaused extends FirestorageEvent {
const FirestorageEventPaused(super.event);
}

class FirestorageEventRunning extends FirestorageEvent {
const FirestorageEventRunning(super.event);
}

class FirestorageEventSuccess extends FirestorageEvent {
const FirestorageEventSuccess(super.event);
Future<String> get downloadURL => _event.ref.getDownloadURL();
}

class FirestorageEventCanceled extends FirestorageEvent {
const FirestorageEventCanceled(super.event);
}

class FirestorageEventError extends FirestorageEvent {
const FirestorageEventError(super.event);
}

sealed class FirestorageEvent {
const FirestorageEvent(this._event);
final TaskSnapshot _event;
int get bytesTransferred => _event.bytesTransferred;
int get totalBytes => _event.totalBytes;

factory FirestorageEvent.fromSnapshot(TaskSnapshot event) => switch (event) {
TaskSnapshot(state: TaskState.paused) => FirestorageEventPaused(event),
TaskSnapshot(state: TaskState.running) =>
FirestorageEventRunning(event),
TaskSnapshot(state: TaskState.success) =>
FirestorageEventSuccess(event),
TaskSnapshot(state: TaskState.canceled) =>
FirestorageEventCanceled(event),
TaskSnapshot(state: TaskState.error) => FirestorageEventError(event),
};
}
23 changes: 23 additions & 0 deletions lib/features/posts/data/firestorage/posts_firestorage.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'dart:io';

import 'package:firebase_storage/firebase_storage.dart';
import 'package:instreal/features/posts/data/firestorage/firestorage_event.dart';

abstract interface class PostsFirestorage {
Stream<FirestorageEvent> upload(File image);
}

class PostsFirestorageImpl implements PostsFirestorage {
PostsFirestorageImpl({FirebaseStorage? storage})
: storageRef = (storage ?? FirebaseStorage.instance).ref("posts");

final Reference storageRef;

@override
Stream<FirestorageEvent> upload(File image) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be implemented properly. I'm thinking of creating a type for the argument that contains metadata + filepath or byte array of the file.

@aliyazdi75 @billyandco What do you guys think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think keeping metadata is useful and byte array.

final task = storageRef.child('image.jpg').putFile(image);
return task.snapshotEvents.map<FirestorageEvent>(
(event) => FirestorageEvent.fromSnapshot(event),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:instreal/features/posts/post_firestore_model.dart';
import 'package:instreal/features/posts/data/firestore/post_firestore_model.dart';

abstract interface class PostsFirestore {
Future<List<PostFirestoreModel>> get posts;
Expand Down
35 changes: 30 additions & 5 deletions lib/features/posts/post_entity.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
import 'dart:io';

import 'package:flutter/material.dart';

@immutable
class Post {
class Post extends PostEntity {
final String id;
final String title;
final String author;
final String imageUrl;

const Post({
required this.id,
required this.title,
required this.author,
required super.title,
required super.author,
required this.imageUrl,
});
}

@immutable
class PostDraft extends PostEntity {
final File imageFile;

const PostDraft({
required super.title,
required super.author,
required this.imageFile,
});

Post toPost(String imageUrl) => Post(
id: "",
title: title,
author: author,
imageUrl: imageUrl,
);
}

@immutable
sealed class PostEntity {
final String title;
final String author;
const PostEntity({required this.title, required this.author});
}
8 changes: 5 additions & 3 deletions lib/features/posts/posts_repository.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import 'package:instreal/features/posts/data/firestorage/posts_firestorage.dart';
import 'package:instreal/features/posts/data/firestore/post_firestore_model.dart';
import 'package:instreal/features/posts/data/firestore/posts_firestore.dart';
import 'package:instreal/features/posts/post_entity.dart';
import 'package:instreal/features/posts/post_firestore_model.dart';
import 'package:instreal/features/posts/posts_firestore.dart';

abstract interface class PostsRepository {
Future<List<Post>> get posts;
Future<String> add(Post posting);
}

class PostsRepositoryImpl implements PostsRepository {
PostsRepositoryImpl({required this.firestore});
PostsRepositoryImpl({required this.firestore, required this.firestorage});

final PostsFirestore firestore;
final PostsFirestorage firestorage;

@override
Future<List<Post>> get posts async =>
Expand Down
12 changes: 6 additions & 6 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
Expand All @@ -16,12 +17,11 @@ void main() async {

if (kDebugMode) {
try {
FirebaseFirestore.instance.useFirestoreEmulator(
defaultTargetPlatform == TargetPlatform.android
? "10.0.2.2"
: "localhost",
8080,
);
final emulatorHost = defaultTargetPlatform == TargetPlatform.android
? "10.0.2.2"
: "localhost";
FirebaseStorage.instance.useStorageEmulator(emulatorHost, 9199);
FirebaseFirestore.instance.useFirestoreEmulator(emulatorHost, 8080);
} catch (e) {
// ignore: avoid_print
print(e);
Expand Down
20 changes: 13 additions & 7 deletions lib/presentation/screens/home/home.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:instreal/features/posts/data/firestorage/posts_firestorage.dart';
import 'package:instreal/features/posts/post_entity.dart';
import 'package:instreal/features/posts/posts_firestore.dart';
import 'package:instreal/features/posts/data/firestore/posts_firestore.dart';
import 'package:instreal/features/posts/posts_repository.dart';
import 'package:instreal/l10n/index.dart';
import 'package:instreal/presentation/components/index.dart';
Expand All @@ -14,7 +17,10 @@ class MyHomePage extends StatefulWidget {
}

class _MyHomePageState extends State<MyHomePage> {
final postingsRepo = PostsRepositoryImpl(firestore: PostFirestoreImpl());
final postingsRepo = PostsRepositoryImpl(
firestore: PostFirestoreImpl(),
firestorage: PostsFirestorageImpl(),
);

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -68,13 +74,13 @@ class _MyHomePageState extends State<MyHomePage> {
floatingActionButton: FloatingActionButton(
onPressed: () async {
await postingsRepo.add(
const Post(
id: "",
PostDraft(
title: 'title',
author: 'author',
imageUrl:
'https://i1.wp.com/www.suitcasescholar.com/wp-content/uploads/2012/08/DSC_2583.jpg',
),
imageFile: File(
Copy link
Member Author

@leehack leehack Jul 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code here doesn't work.
Probably we want to pass bytes instead of File once we read it from the image picker.
@aliyazdi75 @billyandco What do you guys think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing to consider.
Should we separate the image upload and posting in the UI? or do it together?
Should we better create an upload method in postingRepository or does the add method do both?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think we need to pass the bytes instead.

'https://i1.wp.com/www.suitcasescholar.com/wp-content/uploads/2012/08/DSC_2583.jpg',
),
).toPost('imageUrl'),
);
setState(() {});
},
Expand Down
2 changes: 2 additions & 0 deletions macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import Foundation

import cloud_firestore
import firebase_core
import firebase_storage
import path_provider_foundation

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
}
24 changes: 24 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.6.0"
firebase_storage:
dependency: "direct main"
description:
name: firebase_storage
sha256: "195ae2b623ba054b9be3934c79bf6282831ca15a81cfd392841feea930e50253"
url: "https://pub.dev"
source: hosted
version: "11.2.4"
firebase_storage_platform_interface:
dependency: transitive
description:
name: firebase_storage_platform_interface
sha256: be0f4254cae3ccaefd5d1435b82c1fa3bda33187387b241c55b27d7516ea9b93
url: "https://pub.dev"
source: hosted
version: "4.4.3"
firebase_storage_web:
dependency: transitive
description:
name: firebase_storage_web
sha256: f02c63c871deb36180b39a5fab1ee5b2677a01fa0fb642746ca62ae7f9d1a482
url: "https://pub.dev"
source: hosted
version: "3.6.4"
flutter:
dependency: "direct main"
description: flutter
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies:
google_fonts: ^5.0.0
cloud_firestore: ^4.8.1
firebase_core: ^2.14.0
firebase_storage: ^11.2.4

dev_dependencies:
flutter_test:
Expand Down
12 changes: 12 additions & 0 deletions storage.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
rules_version = '2';

// Craft rules based on data in your Firestore database
// allow write: if firestore.get(
// /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin;
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if false;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:instreal/features/posts/post_firestore_model.dart';
import 'package:instreal/features/posts/data/firestore/post_firestore_model.dart';

void main() {
test('convert json to model', () async {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:instreal/features/posts/post_firestore_model.dart';
import 'package:instreal/features/posts/posts_firestore.dart';
import 'package:instreal/features/posts/data/firestorage/firestorage_event.dart';
import 'package:instreal/features/posts/data/firestorage/posts_firestorage.dart';
import 'package:instreal/features/posts/data/firestore/post_firestore_model.dart';
import 'package:instreal/features/posts/data/firestore/posts_firestore.dart';
import 'package:instreal/features/posts/posts_repository.dart';

class MockFirestore implements PostsFirestore {
Expand All @@ -26,16 +30,27 @@ class MockFirestore implements PostsFirestore {
}
}

class MockFireStorage implements PostsFirestorage {
@override
Stream<FirestorageEvent> upload(File image) {
throw UnimplementedError();
}
}

void main() {
test('convert model to entity', () async {
final mockStore = MockFirestore();
final mockStorage = MockFireStorage();
final mockModelData = PostFirestoreModel.fromDocument({
'title': 'title',
'author': 'author',
'imageUrl': 'imageUrl',
}, 'id');
mockStore.add(mockModelData);
final repo = PostsRepositoryImpl(firestore: mockStore);
final repo = PostsRepositoryImpl(
firestore: mockStore,
firestorage: mockStorage,
);
final posts = await repo.posts;
expect(posts.length, 1);
expect(posts[0].id, 'id');
Expand Down