Flutter 와 Firebase 을 이용한 간단 Note App 만들기
https://firebase.google.com/docs/auth/android/google-signin#before_you_begin
https://en.wikipedia.org/wiki/Reactive_programming
root gatekeeper widget
https://firebase.google.com/docs/firestore
저는 https://www.google.com/keep 의 팬입니다 . 출시 된 이래로 계속 사용하고 있습니다. 보류중인 작업, 집안일에 대한 알림, 거의 기억해야 할 모든 내용을 Keep에 넣었습니다. 사용이 직관적이며 우선 순위에 집중할 수 있도록 도와줍니다.지난 2 년 동안 https://flutter.dev/ Flutter 앱을 개발해 왔으므로 Keep from scratch와 같은 노트북 앱을 만드는 것이 흥미로울 것 같습니다.
내가 지금까지 만든 소위 ' Flutter Keep ' 앱은 다음과 같습니다.
이 과정을 일련의 기사로 소개하겠습니다. 반복하는 동안 주요 구성 요소가 앱에 추가되어 이해하기 쉽습니다.
이 시리즈의 첫 번째 부분에서는 Flutter 프로젝트를 설정하고 인증 프로세스를 제공하며 메모 목록을 표시하는 간단한 화면을 제공합니다.
시작해 봅시다!!
프로젝트를 만들기 전에 Android 및 iOS 이외의 웹에서 앱을 실행할 수있게 코멘트를 실행해야 할 수 있습니다.
command : flutter config -- enable-web
이제 Flutter Keep 앱을 만들려면 다음 명령을 실행 하십시오
command : flutter create flt_keep
flt_keep 는 import 문에 사용될 패키지 이름입니다
Flutter를 처음 사용 하는 사람들은 https://flutter.dev/docs/get-started/install 시작 안내서를 따라 SDK를 설치하고 프로젝트 구조에 익숙해 지십시오.
노트북 앱의 경우 가장 먼저 고려해야 할 것은 메모를 유지하고 쿼리하는 방법입니다.
내가 중점적으로 고려하는 것은 다음과 같습니다.
첫째, 프라이버시. 다른 계정의 메모는 서로 분리되어야합니다.
둘째, 앱이 오프라인에서 작동해야합니다. 사용자는 모든 네트워크 조건에서 메모를 할 수 있어야합니다. 네트워크 연결이 복구 될 때 데이터를 동기화하는 것은 앱의 책임입니다.
내가 선택한 것은 https://firebase.google.com/docs/firestore Cloud Firestore 입니다 . 주로 이전 프로젝트에서 얻은 경험 때문이지만 적응하기도 쉽기 때문입니다.
저는 전용 모음을 사용하여 각 사용자의 메모, 단일 메모에 대한 하나의 문서를 저장하기로 결정했습니다. 그 이유는 다음과 같습니다 :
각 계정 데이터의 더 나은 분리
쉬운 쿼리
데이터 읽기 및 쓰기의 일부 https://firebase.google.com/docs/firestore/quotas#limits 제한을 회피 가능
이 방법은 비용도 함께 제공되지만 시연 목적으로도 허용됩니다. 즉, 각 컬렉션에 대해 인덱스를 동적으로 만들어야하므로 이후 기사에서이 문제에 대해 설명하겠습니다.
현재 데이터 구조는 다음과 같습니다.
앱 아키텍처
이제 앱 로직을 구성하는 방법을 고려해야합니다. 시연을 위해 앱에 '실제'아키텍처를 적용하는 것은 가치가 없습니다. 그러나 여전히 앱의 여러 화면에서 상태를 관리해야합니다.
이 경우 https://pub.dev/packages/provider 제공자 패키지를 사용하여 앱 상태를 관리합니다. 반응형 (또는 데이터 중심) 스타일로 코드를 작성할 수 있습니다.
앱에서 가장 중요한 화면은 다음과 같습니다.
로그인 상태를 확인하는 인증 화면은 인증 된 사용자만 메모 할 수 있도록 합니다.
최신 메모 상태를 표시하는 메모 목록 화면은 모든 메모 변경에 적절하게 반응해야 합니다.
노트 편집기는 편집중인 특정 노트의 외부 수정에 적절하게 반응해야 합니다.
https://pub.dev/packages/provider 제공자를 통해 보다 깨끗한 코드베이스로 위의 요구 사항을보다 쉽게 충족 할 수 있습니다.
Scheme 를 염두에 두고, 이제 코드 작성을 시작하겠습니다.
우리가 provider 와 Firebase SDK 를 사용하려면, pubspec.yaml 파일에 종속성을 추가해야 해야 합니다.
provider: ^4.0.2
firebase_core: ^0.4.4
firebase_auth: ^0.15.4
cloud_firestore: ^0.13.3
google_sign_in: ^4.1.4
Android 및 iOS 용 Firebase SDK와 웹 플랫폼(https://firebase.google.com/docs/web/setup)을 통합하는 것은 이 지침(https://firebase.google.com/docs/flutter/setup)에 따라 주세요.
반드시 기억해야할 것은 인증되지 않은 사용자는 거부 해야합니다. 루트에 gatekeeper 위젯을 작성하십시오.
main.dart
void main() => runApp(NotesApp());
/// Root widget of the application.
class NotesApp extends StatelessWidget {
@override
Widget build(BuildContext context) => StreamProvider.value(
initialData: CurrentUser.initial,
value: FirebaseAuth.instance.onAuthStateChanged.map((user) => CurrentUser.create(user)),
child: Consumer<CurrentUser>(
builder: (context, user, _) => MaterialApp(
title: 'Flutter Keep',
home: user.isInitialValue
? Scaffold(body: const Text('Loading...'))
: user.data != null ? HomeScreen() : LoginScreen(),
),
),
);
}
여기서 StreamProvider/ Consumer pair 하는 것은 onAuthStateChanged 을 보기 위함이며,
Firebase 인증 이벤트들의 스트림과 현재의 상태에 따라 통보 및 재 빌드 위젯 얻을 것이다.
Customer 는 알람정보를 얻고, 현재 상태을 적용하는 위젯을 re-Build 하게 됩니ㅏ다.
아래 소스에서 약간의 트릭은 FirebaseUser 을 래핑하기 위해서 초기값과 비인증한 상태를 구분하기 위해서 CusttentUser 을 사용하는데, 모두 Null 으로 만듭니다.
current_user.dart
class CurrentUser {
final bool isInitialValue;
final FirebaseUser data;
const CurrentUser._(this.data, this.isInitialValue);
factory CurrentUser.create(FirebaseUser data) => CurrentUser._(data, false);
/// The inital empty instance.
static const initial = CurrentUser._(null, true);
}
통합하기 쉽기 때문에 Google 로그인을 예로 사용합니다. 실제로 Firebase Auth가 지원하는 많은 서비스 중 하나 일뿐입니다. Firebase 콘솔에서 필요한 기능을 활성화 할 수 있습니다.
Google 로그인을 사용하여 인증하는 code snippet
: login_screen.dart
class LoginScreen extends StatefulWidget {
@override
State<StatefulWidget> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _auth = FirebaseAuth.instance;
final _googleSignIn = GoogleSignIn();
String _errorMessage;
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: Column(
children: <Widget>[
RaisedButton(
onPressed: _signInWithGoogle,
child: const Text('Continue with Google'),
),
if (_errorMessage != null) Text(
_errorMessage,
style: const TextStyle(color: Colors.red),
),
],
),
),
);
void _signInWithGoogle() async {
_setLoggingIn(); // show progress
String errMsg;
try {
final googleUser = await _googleSignIn.signIn();
final googleAuth = await googleUser.authentication;
final credential = GoogleAuthProvider.getCredential(
idToken: googleAuth.idToken,
accessToken: googleAuth.accessToken,
);
await _auth.signInWithCredential(credential);
} catch (e) {
errMsg = 'Login failed, please try again later.';
} finally {
_setLoggingIn(false, errMsg); // always stop the progress indicator
}
}
/// update the logging-in indicator, & show error message if any
void _setLoggingIn([bool loggingIn = true, String errMsg]) {
if (mounted) {
setState(() {
_loggingIn = loggingIn;
_errorMessage = errMsg;
});
}
}
}
Google 로그인 자격 증명을 요청한 다음이를 사용하여 Firebase 인증을 받으십시오.
당신은 인증이 완료된 후에는 수행 할 작업이 없음을 알 수 있습니다.
이 상황에서 FirebaseAuth.onAuthStateChanged 스트림은 'Signed-in' 이벤트를 생성하여 root gatekeeper widget 의 재 빌드를 트리거하여 HomeScreen 이 렌더링됩니다.
위의 예제는 https://en.wikipedia.org/wiki/Reactive_programming 리 액티브 프로그래밍의 예제입니다 . 상태를 변경하면 상태에 관심이있는 리스너가 나머지 작업을 수행합니다.
프로젝트로 돌아가서 로그인 화면을 테스트하기 전에 다음 설정을 무시하지 않아야합니다.
Android 플랫폼의 경우 Firebase 콘솔에서 https://firebase.google.com/docs/auth/android/google-signin#before_you_begin SHA-1 fingerprint 지정 해야합니다.
iOS 플랫폼의 경우 Xcode 프로젝트에 https://firebase.google.com/docs/auth/ios/google-signin#2_implement_google_sign-in 사용자 정의 URL sheme 을 추가 해야합니다.
웹 플랫폼의 경우, web/index.html 에 같은 메타 태그를 <meta name="google-signin-client_id" content="{web_client_id}"> 추가해야 합니다. 이를 통해서 당신은 'OAuth 2.0 Client IDs’ https://console.cloud.google.com/apis/credentials 자격 증명 페이지 Google 클라우드 콘솔을 찾을 수 있습니다.
인증 된 사용자를 통해 이제 앱의 기본 화면 인 메모 목록을 입력 할 수 있습니다.
그러나 메모 편집기없이 첫 번째 메모를 어떻게 추가 할 수 있습니까? Firebase 콘솔을 사용하여이 작업을 수행 할 수 있습니다.
notes-{user_id} 처럼 컬렉션 이름을 지정하면 Firebase 콘솔의 인증 페이지에서 사용자 ID를 찾을 수 있습니다
개인 정보 보안을 강화하기 위해 데이터 세트에 대한 액세스 규칙을 설정하여 사용자가 자신의 메모 만보고 편집 할 수 있도록 할 수 있습니다.
Firestore에서 메모를 검색하려면, 개별 메모를 나타내는 모델과 Firestore 모델과 자체 메모를 변환하는 기능이 필요합니다.
note.dart
class Note {
final String id;
String title;
String content;
Color color;
NoteState state;
final DateTime createdAt;
DateTime modifiedAt;
/// Instantiates a [Note].
Note({
this.id,
this.title,
this.content,
this.color,
this.state,
DateTime createdAt,
DateTime modifiedAt,
}) : this.createdAt = createdAt ?? DateTime.now(),
this.modifiedAt = modifiedAt ?? DateTime.now();
/// Transforms the Firestore query [snapshot] into a list of [Note] instances.
static List<Note> fromQuery(QuerySnapshot snapshot) => snapshot != null ? toNotes(snapshot) : [];
}
/// State enum for a note.
enum NoteState {
unspecified,
pinned,
archived,
deleted,
}
/// Transforms the query result into a list of notes.
List<Note> toNotes(QuerySnapshot query) => query.documents
.map((d) => toNote(d))
.where((n) => n != null)
.toList();
/// Transforms a document into a single note.
Note toNote(DocumentSnapshot doc) => doc.exists
? Note(
id: doc.documentID,
title: doc.data['title'],
content: doc.data['content'],
state: NoteState.values[doc.data['state'] ?? 0],
color: _parseColor(doc.data['color']),
createdAt: DateTime.fromMillisecondsSinceEpoch(doc.data['createdAt'] ?? 0),
modifiedAt: DateTime.fromMillisecondsSinceEpoch(doc.data['modifiedAt'] ?? 0),
)
: null;
Color _parseColor(num colorInt) => Color(colorInt ?? 0xFFFFFFFF);
다시 우리는 HomeScreen 에서 StreamProvider을 사용해야 하며, 이를 통해서 어떠한 변화가 백엔드 일어날 것을 알고 바로 여기에 노트 쿼리 결과를 노트에 반영할 수 있도록 계속해서 감시하게 됩니다. Firestore SDK는 또한 필요한 https://firebase.google.com/docs/firestore/manage-data/enable-offline오프라인 기능 을 제공하므로 데이터 액세스에 사용되는 코드를 변경할 필요가 없습니다. 이전에 구축한 gatekeeper widget 덕분에 Provider.of<CurrentUser> 대비해서 언제든지 인증 정보를 검색 할 수 있습니다.
home_screen.dart
/// Home screen, displays [Note] in a Grid or List.
class HomeScreen extends StatefulWidget {
@override
State<StatefulWidget> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool _gridView = true; // `true` to show a Grid, otherwise a List.
@override
Widget build(BuildContext context) => StreamProvider.value(
value: _createNoteStream(context),
child: Scaffold(
body: CustomScrollView(
slivers: <Widget>[
_appBar(context), // a floating appbar
const SliverToBoxAdapter(
child: SizedBox(height: 24), // top spacing
),
_buildNotesView(context),
const SliverToBoxAdapter(
child: SizedBox(height: 80.0), // bottom spacing make sure the content can scroll above the bottom bar
),
],
),
floatingActionButton: _fab(context),
bottomNavigationBar: _bottomActions(),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
extendBody: true,
),
);
/// A floating appBar like the one of Google Keep
Widget _appBar(BuildContext context) => SliverAppBar(
floating: true,
snap: true,
title: _topActions(context),
automaticallyImplyLeading: false,
centerTitle: true,
titleSpacing: 0,
backgroundColor: Colors.transparent,
elevation: 0,
);
Widget _topActions(BuildContext context) => Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Row(
children: <Widget>[
const SizedBox(width: 20),
const Icon(Icons.menu),
const Expanded(
child: Text('Search your notes', softWrap: false),
),
InkWell(
child: Icon(_gridView ? Icons.view_list : Icons.view_module),
onTap: () => setState(() {
_gridView = !_gridView; // switch between list and grid style
}),
),
const SizedBox(width: 18),
_buildAvatar(context),
const SizedBox(width: 10),
],
),
),
),
);
Widget _bottomActions() => BottomAppBar(
shape: const CircularNotchedRectangle(),
child: Container(
height: kBottomBarSize,
padding: const EdgeInsets.symmetric(horizontal: 17),
child: Row(
...
),
),
);
Widget _fab(BuildContext context) => FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {},
);
Widget _buildAvatar(BuildContext context) {
final url = Provider.of<CurrentUser>(context)?.data?.photoUrl;
return CircleAvatar(
backgroundImage: url != null ? NetworkImage(url) : null,
child: url == null ? const Icon(Icons.face) : null,
radius: 17,
);
}
/// A grid/list view to display notes
Widget _buildNotesView(BuildContext context) => Consumer<List<Note>>(
builder: (context, notes, _) {
if (notes?.isNotEmpty != true) {
return _buildBlankView();
}
final widget = _gridView ? NotesGrid.create : NotesList.create;
return widget(notes: notes, onTap: (_) {});
},
);
Widget _buildBlankView() => const SliverFillRemaining(
hasScrollBody: false,
child: Text('Notes you add appear here',
style: TextStyle(
color: Colors.black54,
fontSize: 14,
),
),
);
/// Create the notes query
Stream<List<Note>> _createNoteStream(BuildContext context) {
final uid = Provider.of<CurrentUser>(context)?.data?.uid;
return Firestore.instance.collection('notes-$uid')
.where('state', isEqualTo: 0)
.snapshots()
.handleError((e) => debugPrint('query notes failed: $e'))
.map((snapshot) => Note.fromQuery(snapshot));
}
}
코드는 조금 장황합니다. 여기 에 Google Keep 에서 의 floating AppBar 제공합니다.
NotesGrid 와 NotesList 을 위해서, 그들은 매우 비슷한 것 같습니다. 단지 각각 SliverGrid 와 SliverList 의 wrappr 의 한 종류 입니다.
notes_grid.dart
class NotesGrid extends StatelessWidget {
final List<Note> notes;
final void Function(Note) onTap;
const NotesGrid({Key key, this.notes, this.onTap}) : super(key: key);
/// A static factory method can be used as a function reference
static NotesGrid create({Key key, this.notes, this.onTap}) =>
NotesGrid(key: key, notes: notes, onTap: onTap);
@override
Widget build(BuildContext context) => SliverGrid(
...
);
}
자세한 코드를 모두 여기에 게시하지는 않습니다. 내 https://github.com/xinthink/flutter-keep GitHub 저장소 에서 전체 예제를 찾으십시오 .
모든 것이 잘되면, 이제 직접 만든 Flutter Keep 앱 에서 첫 번째 노트를 볼 수 있습니다!
우리는 지금까지 잘하고 있습니다. provider 패키지를 사용하여 간단한 반응 형 앱을 구축하고 Firebase 툴킷을 사용하는 방법도 배웠습니다.
그러나 응용 프로그램은 메모 편집기가 없으면 유용하지 않습니다. 시리즈의 다음 부분에서 더 많은 기능을 추가 할 것입니다.
To Be Continued ....