brunch

You can make anything
by writing

C.S.Lewis

by 이종우 Peter Lee Mar 06. 2020

[번역]Flutter+Firebase noteapp 1

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://medium.com/flutter-community/build-a-note-taking-app-with-flutter-firebase-part-i-53816e7a3788


Flutter + Firebase를 사용하여 메모 작성 앱 구축 — 1 부


Google Keep의 단순화 된 '복제본'을 처음부터 만드는 방법으로 해보겠습니다.

Flutter Keep


저는 https://www.google.com/keep 의 팬입니다 . 출시 된 이래로 계속 사용하고 있습니다. 보류중인 작업, 집안일에 대한 알림, 거의 기억해야 할 모든 내용을 Keep에 넣었습니다. 사용이 직관적이며 우선 순위에 집중할 수 있도록 도와줍니다.지난 2 년 동안 https://flutter.dev/ Flutter 앱을 개발해 왔으므로 Keep from scratch와 같은 노트북 앱을 만드는 것이 흥미로울 것 같습니다. 


내가 지금까지 만든 소위 ' Flutter Keep ' 앱은 다음과 같습니다.      

https://youtu.be/GXNXodzgbcM

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 제한을 회피 가능 


이 방법은 비용도 함께 제공되지만 시연 목적으로도 허용됩니다. 즉, 각 컬렉션에 대해 인덱스를 동적으로 만들어야하므로 이후 기사에서이 문제에 대해 설명하겠습니다.


현재 데이터 구조는 다음과 같습니다.      


Firestore data structure


앱 아키텍처

이제 앱 로직을 구성하는 방법을 고려해야합니다. 시연을 위해 앱에 '실제'아키텍처를 적용하는 것은 가치가 없습니다. 그러나 여전히 앱의 여러 화면에서 상태를 관리해야합니다.


이 경우 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(),

      ),

    ),

  );

}


여기서 StreamProviderConsumer  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 인증

통합하기 쉽기 때문에 Google 로그인을 예로 사용합니다. 실제로 Firebase Auth가 지원하는 많은 서비스 중 하나 일뿐입니다. Firebase 콘솔에서 필요한 기능을 활성화 할 수 있습니다.            


Available authentication providers


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 콘솔을 사용하여이 작업을 수행 할 수 있습니다.

Firestore console



notes-{user_id} 처럼 컬렉션 이름을 지정하면 Firebase 콘솔의 인증 페이지에서 사용자 ID를 찾을 수 있습니다


개인 정보 보안을 강화하기 위해 데이터 세트에 대한 액세스 규칙을 설정하여 사용자가 자신의 메모 만보고 편집 할 수 있도록 할 수 있습니다.


Firestore data access rules


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 앱 에서 첫 번째 노트를 볼 수 있습니다!            


Flutter Keep screenshot


우리는 지금까지 잘하고 있습니다. provider 패키지를 사용하여 간단한 반응 형 앱을 구축하고 Firebase 툴킷을 사용하는 방법도 배웠습니다.


그러나 응용 프로그램은 메모 편집기가 없으면 유용하지 않습니다. 시리즈의 다음 부분에서 더 많은 기능을 추가 할 것입니다.


To Be Continued .... 


https://www.twitter.com/FlutterComm

매거진의 이전글 [번역] 빠르게 코딩을 배우는 팁들
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari