이 프로젝트는 개발하는남자님의 유튜브 영상을 참고하여 제작하였습니다. 허나, 원본 영상에서 제작하는 방법과는 다를 수 있습니다.
가장 간단하게 서버를 클라이언트에 연결할 수 있는 클라우드 서비스 중 파이어베이스가 있습니다. 서버를 직접 구축하지 않아도 서버의 기능을 일정용량은 무료로 사용할 수 있어요. 파이어베이스와 연결하면서 DB에 연결하는 방법에 대해서 알아보도록 하겠습니다.
단, 프로젝트의 파이어베이스 연동은 완료되었다는 가정하에 시작하겠습니다. 기본적으로 연결하는 방법은 아래 링크를 참조해주시면 좋을 것 같습니다.
데이터베이스는 파이어베이스의 Cloud Firestore를 사용하도록 하겠습니다. 제가 데이터베이스를 통해서 가져올 데이터는 바로 피드입니다. 피드는 사실 깊게 파보면 더 많은 정보들을 포함하고 있지만, 간단한 Entity 제작을 위해서 기존에 만들었던 구성대로만 Entity로 만들도록 하겠습니다. 자, 이제 시작하겠습니다.
프로젝트에서 Cloud Firestore를 사용하기 위해서는 패키지가 필요합니다.
$ flutter pub add cloud_firestore
파이어베이스 패키지를 프로젝트에 추가하는 경우 MacOS 사용자는 Podfile을 업데이트할 필요가 있으니 참고해주세요.
이것은 제가 만든 임의의 데이터입니다. 홈 화면에서 나타나는 피드의 정보를 그대로 넣어줬어요. 이제 모든 준비가 끝났습니다.
DB로 접근하려는 데이터는 Model을 만들어서 많이 사용해요. 유지보수성이 굉장히 뛰어나기 때문입니다. Model을 정의하지 않으면 해당 내용이 변경되었을 경우 너무 많은 파일에서 에러가 발생하게 됩니다. 그래서 Feed라는 데이터는 Feed라는 Model을 생성해서 담아주겠습니다.
import 'package:cloud_firestore/cloud_firestore.dart';
class Feed {
final String id;
final String userUrl;
final String userName;
final List images;
final int countLike;
final int countComment;
Feed({
required this.id,
required this.userUrl,
required this.userName,
required this.images,
required this.countLike,
required this.countComment,
});
factory Feed.fromDocumentSnapshot(DocumentSnapshot doc) {
return Feed(
id: doc.id,
userUrl: doc['userUrl'],
userName: doc['userName'],
images: doc['images'],
countLike: doc['countLike'],
countComment: doc['countComment']);
}
}
Feed는 일반적으로 Json 데이터가 아니라 DocumentSnapshot을 통해 가져올 예정입니다. 그래서 FromDocumentSnaphot 메소드를 추가했습니다.
Provider는 Provider 패키지를 의미하는게 아니라, 실질적으로 Controller의 비즈니스 로직을 담당하는 역할입니다. 어떻게 보면 Endpoint라고 할 수 있습니다.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_instagram/src/data/model/feed.dart';
class FirebaseDB {
final FirebaseFirestore firebaseFirestore;
FirebaseDB({
required this.firebaseFirestore,
});
Stream<List<Feed>> getFeeds() {
try {
return firebaseFirestore
.collection('feeds')
.snapshots()
.map((querySnapshot) {
List<Feed> feeds = [];
for (var feed in querySnapshot.docs) {
final feedModel = Feed.fromDocumentSnapshot(feed);
feeds.add(feedModel);
}
return feeds;
});
} catch (e) {
throw Exception();
}
}
}
Repository는 DB에 접근을 하기 위해 필요한 객체입니다. Repository는 Provider에 정의된 api를 위한 객체을 DI해서 사용하게 됩니다.
class FeedRepository {
final FirebaseDB firebaseDB;
FeedRepository({required this.firebaseDB});
Stream<List<Feed>> getFeeds() => firebaseDB.getFeeds();
}
이제 대망의 Controller입니다. Controller에서는 Repository를 DI해서 사용하며, onReady를 통해 데이터를 fetch하게 됩니다.
class FeedController extends GetxController {
final Rx<List<Feed>> _feeds = Rx<List<Feed>>([]);
final FeedRepository feedRepository;
FeedController({required this.feedRepository});
List<Feed> get feeds => _feeds.value;
void onReady() {
super.onReady();
_feeds.bindStream(_fetchData());
}
Stream<List<Feed>> _fetchData() => feedRepository.getFeeds();
}
GetX는 bindStream 덕분에 아주 간편하게 데이터의 Stream을 전달해줄 수 있습니다.
Feed 위젯은 더이상 프로퍼티를 분리할 필요 없이 Feed를 전달받으면 간편하게 랜더할수 있습니다. 후에 Feed의 내용이 변경되더라도 위젯의 변경용이성이 좋겠죠?
class FeedWidget extends StatefulWidget {
final Feed feed; // 변경
const FeedWidget({
super.key,
required this.feed,
});
State<FeedWidget> createState() => _FeedWidgetState();
}
View에서는 GetX를 통해 간편하게 화면에 Controller의 변경데이터를 전달해줄 수 있습니다.
Widget _body() {
return GetX<FeedController>(builder: (controller) {
return SliverList.builder(
itemCount: controller.feeds.length,
itemBuilder: (context, index) {
final feed = controller.feeds[index];
return FeedWidget(feed: feed);
});
});
}
새로 생성한 FeedController는 InitBinding에서 생성하도록 하겠습니다.
void dependencies() {
Get.put(BottomNavigationController());
Get.put(HomeController());
//추가
Get.put(FeedController(
feedRepository: FeedRepository(
firebaseDBapi:
FirebaseDBApi(firebaseFirestore: FirebaseFirestore.instance))));
이제, 앱을 새로고침하고 결과를 확인하겠습니다.
성공적으로 데이터베이스에 액세스하였습니다.