Flutter 튜토리얼: 첫 Flutter 앱을 만드는 방법

게시 됨: 2022-03-11

플러터란?

Flutter는 두 개의 별도 코드베이스를 유지할 필요 없이 제품이 Android 및 iOS 플랫폼을 동시에 대상으로 할 수 있도록 하는 Google의 모바일 앱 개발 SDK입니다. 또한 Flutter를 사용하는 앱은 Google의 곧 출시될 Fuchsia 운영 체제를 대상으로 컴파일할 수도 있습니다.

Flutter는 최근 중요한 이정표인 안정적인 버전 1.0에 도달했습니다. 출시는 2018년 12월 5일 런던 Flutter Live 이벤트에서 이루어졌습니다. 아직 초기의 진화하는 소프트웨어 벤처로 간주될 수 있지만 이 기사에서는 이미 입증된 개념에 초점을 맞추고 Flutter 1.2와 Firebase를 사용하여 두 주요 모바일 플랫폼을 대상으로 하는 완전한 기능의 메시징 앱을 개발하는 방법을 보여줍니다.

아래 차트에서 볼 수 있듯이 Flutter는 최근 몇 달 동안 많은 사용자를 확보하고 있습니다. 2018년에 Flutter의 시장 점유율은 두 배로 증가했으며 검색 쿼리 측면에서 React Native를 능가할 예정이므로 새로운 Flutter 자습서를 만들기로 결정했습니다.

2018년 7월부터 9월까지 Flutter와 React 사용자를 비교한 차트입니다.

참고: 이 문서는 구현의 특정 부분에만 초점을 맞춥니다. 프로젝트에 대한 전체 소스 코드 참조는 이 GitHub 리포지토리에서 찾을 수 있습니다.

전제 조건

독자들이 모바일 개발을 처음 시도하더라도 이 프로젝트를 따라하고 달성할 수 있도록 많은 노력을 기울였음에도 불구하고 Flutter에 국한되지 않은 핵심 모바일 개발 개념이 많이 언급되고 자세한 설명 없이 사용됩니다.

독자가 한 번에 프로젝트를 완료하는 것이 목표 중 하나이기 때문에 이것은 기사의 간결성을 위해 수행되었습니다. 마지막으로 이 기사에서는 필요한 Android Studio 플러그인 및 Flutter SDK를 포함하여 개발 환경이 이미 설정되어 있다고 가정합니다.

Firebase 설정

Firebase 설정은 각 플랫폼에 대해 독립적으로 수행해야 하는 유일한 작업입니다. 먼저 Firebase 대시보드에서 새 프로젝트를 생성하고 새로 생성된 작업 공간에 Android 및 iOS 애플리케이션을 추가해야 합니다. 플랫폼은 Android용 google-services.json 및 iOS용 GoogleService-Info.plist 라는 두 가지 구성 파일을 생성하여 다운로드해야 합니다. 대시보드를 닫기 전에 Firebase 및 Google 인증 공급자를 활성화해야 사용자 식별에 사용할 수 있습니다. 이렇게 하려면 메뉴에서 인증 항목을 선택한 다음 로그인 방법 탭을 선택합니다.

이제 나머지 설정이 코드베이스에서 수행되므로 대시보드를 닫을 수 있습니다. 먼저 다운로드한 파일을 프로젝트에 넣어야 합니다. google-services.json 파일은 $(FLUTTER_PROJECT_ROOT)/android/app 폴더에, GoogleService-Info.plist$(FLUTTER_PROJECT_ROOT)/ios/Runner 디렉토리에 있어야 합니다. 다음으로 프로젝트에서 사용할 Firebase 라이브러리를 실제로 설정하고 구성 파일과 연결해야 합니다. 이것은 프로젝트의 pubspec.yaml 파일에서 사용할 Dart 패키지(라이브러리)를 지정하여 수행됩니다. 파일의 종속성 섹션에 다음 스니펫을 붙여넣습니다.

 flutter_bloc: shared_preferences: firebase_auth: cloud_firestore: google_sign_in: flutter_facebook_login:

처음 두 개는 Firebase와 관련이 없지만 프로젝트에서 자주 사용할 것입니다. 마지막 두 가지는 아마도 자명합니다.

마지막으로 인증 흐름을 성공적으로 완료할 수 있도록 플랫폼별 프로젝트 설정을 구성해야 합니다. Android 측에서는 google-services Gradle 플러그인을 프로젝트 수준 Gradle 구성에 추가해야 합니다. 즉, $(FLUTTER_PROJECT_ROOT)/android/build.gradle 파일의 종속성 목록에 다음 항목을 추가해야 합니다.

 classpath 'com.google.gms:google-services:4.2.0' // change 4.2.0 to the latest version

그런 다음 $(FLUTTER_PROJECT_ROOT)/android/app/build.gradle 끝에 다음 줄을 추가하여 해당 플러그인을 적용해야 합니다.

 apply plugin: 'com.google.gms.google-services'

이 플랫폼의 마지막 작업은 Facebook 애플리케이션 매개변수를 등록하는 것입니다. 여기서 찾고 있는 것은 $(FLUTTER_PROJECT_ROOT)/android/app/src/main/AndroidManifest.xml$(FLUTTER_PROJECT_ROOT)/android/app/src/main/res/values/strings.xml 의 두 파일을 편집하는 것입니다.

 <!-- AndroidManifest.xml --> <manifest xmlns:androcom.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/> <activity android:name="com.facebook.FacebookActivity" android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" android:label="@string/app_name" /> <activity android:name="com.facebook.CustomTabActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="@string/fb_login_protocol_scheme" /> </intent-filter> </activity> <!-- … --> </application> </manifest> <!-- strings.xml --> <resources> <string name="app_name">Toptal Chat</string> <string name="facebook_app_id">${YOUR_FACEBOOK_APP_ID}</string> <string name="fb_login_protocol_scheme">${YOUR_FACEBOOK_URL}</string> </resources>

이제 iOS를 사용할 차례입니다. 운 좋게도 이 경우에는 하나의 파일만 변경하면 됩니다. $(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist 에 다음 값을 추가합니다( CFBundleURLTypes 항목이 목록에 이미 있을 수 있습니다. 이 경우 다시 선언하는 대신 기존 배열에 이러한 항목을 추가해야 함). 파일:

 <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>${YOUR_FACEBOOK_URL}</string> </array> </dict> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLSchemes</key> <array> <string>${YOUR_REVERSED_GOOGLE_WEB_CLIENT_ID}</string> </array> </dict> </array> <key>FacebookAppID</key> <string>${YOUR_FACEBOOK_APP_ID}</string> <key>FacebookDisplayName</key> <string>${YOUR_FACEBOOK_APP_NAME}</string> <key>LSApplicationQueriesSchemes</key> <array> <string>fbapi</string> <string>fb-messenger-share-api</string> <string>fbauth2</string> <string>fbshareextension</string> </array>

BLoC 아키텍처에 대한 한마디

이 아키텍처 표준은 Flutter 및 AngularDart에서 코드 공유를 위한 BLoC 사용을 보여주는 이전 기사 중 하나에서 설명되었으므로 여기서는 자세히 설명하지 않겠습니다.

주요 아이디어의 기본 아이디어는 모든 화면에 다음 클래스가 있다는 것입니다. - 보기 - 현재 상태를 표시하고 사용자 입력을 이벤트로 블록에 위임하는 역할을 합니다. - 상태 - 사용자가 현재 보기를 사용하여 상호 작용하는 "라이브" 데이터를 나타냅니다. - bloc - 이벤트에 응답하고 그에 따라 상태를 업데이트하며 선택적으로 하나 이상의 로컬 또는 원격 저장소에서 데이터를 요청합니다. - 이벤트 - 현재 상태를 변경하거나 변경하지 않을 수 있는 확실한 조치 결과입니다.

그래픽 표현으로 다음과 같이 생각할 수 있습니다.

Flutter 튜토리얼: BLoC 아키텍처의 그래픽 표현.

또한 데이터 클래스와 이러한 클래스의 인스턴스를 생성하는 리포지토리가 포함된 모델 디렉터리가 있습니다.

UI 개발

Flutter를 사용하여 UI 생성은 완전히 Dart에서 이루어집니다. 반면에 UI는 XML 체계를 사용하여 빌드되고 비즈니스 로직 코드베이스와 완전히 분리되는 Android 및 iOS의 기본 앱 개발과 대조됩니다. 현재 상태(예: isLoading, isEmpty 매개변수)에 따라 다른 구성요소가 있는 비교적 간단한 UI 요소 구성을 사용할 것입니다. Flutter의 UI는 위젯, 즉 위젯 트리를 중심으로 회전합니다. 위젯은 상태 비저장 또는 상태 저장이 될 수 있습니다. stateful 위젯의 경우 setState() 가 현재 표시된 특정 위젯에서 호출될 때(생성자에서 호출하거나 삭제된 후 런타임 오류가 발생함) 빌드 및 그리기 단계가 다음과 같다는 점을 강조하는 것이 중요합니다. 다음 드로잉 주기에 수행될 예정입니다.

간결함을 위해 여기서는 UI(보기) 클래스 중 하나만 표시합니다.

 class LoginScreen extends StatefulWidget { LoginScreen({Key key}) : super(key: key); @override State<StatefulWidget> createState() => _LoginState(); } class _LoginState extends State<LoginScreen> { final _bloc = LoginBloc(); @override Widget build(BuildContext context) { return BlocProvider<LoginBloc>( bloc: _bloc, child: LoginWidget(widget: widget, widgetState: this) ); } @override void dispose() { _bloc.dispose(); super.dispose(); } } class LoginWidget extends StatelessWidget { const LoginWidget({Key key, @required this.widget, @required this.widgetState}) : super(key: key); final LoginScreen widget; final _LoginState widgetState; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Login"), ), body: BlocBuilder( bloc: BlocProvider.of<LoginBloc>(context), builder: (context, LoginState state) { if (state.loading) { return Center( child: CircularProgressIndicator(strokeWidth: 4.0) ); } else { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ ButtonTheme( minWidth: 256.0, height: 32.0, child: RaisedButton( onPressed: () => BlocProvider.of<LoginBloc>(context).onLoginGoogle(this), child: Text( "Login with Google", style: TextStyle(color: Colors.white), ), color: Colors.redAccent, ), ), ButtonTheme( minWidth: 256.0, height: 32.0, child: RaisedButton( onPressed: () => BlocProvider.of<LoginBloc>(context).onLoginFacebook(this), child: Text( "Login with Facebook", style: TextStyle(color: Colors.white), ), color: Colors.blueAccent, ), ), ], ), ); } }), ); } void navigateToMain() { NavigationHelper.navigateToMain(widgetState.context); } }

나머지 UI 클래스는 동일한 패턴을 따르지만 다른 작업을 가질 수 있으며 로드 상태 외에 빈 상태 위젯 트리를 특징으로 할 수 있습니다.

입증

짐작하셨겠지만, 우리는 google_sign_influtter_facebook_login 라이브러리를 사용하여 소셜 네트워크 프로필에 의존하여 사용자를 인증할 것입니다. 우선, 로그인 요청 로직을 처리할 파일로 이러한 패키지를 가져와야 합니다.

 import 'package:flutter_facebook_login/flutter_facebook_login.dart'; import 'package:google_sign_in/google_sign_in.dart';

이제 인증 흐름을 처리할 두 개의 독립적인 부분이 있을 것입니다. 첫 번째는 Facebook 또는 Google 로그인 요청을 시작하는 것입니다.

 void onLoginGoogle(LoginWidget view) async { dispatch(LoginEventInProgress()); final googleSignInRepo = GoogleSignIn(signInOption: SignInOption.standard, scopes: ["profile", "email"]); final account = await googleSignInRepo.signIn(); if (account != null) { LoginRepo.getInstance().signInWithGoogle(account); } else { dispatch(LogoutEvent()); } } void onLoginFacebook(LoginWidget view) async { dispatch(LoginEventInProgress()); final facebookSignInRepo = FacebookLogin(); final signInResult = await facebookSignInRepo.logInWithReadPermissions(["email"]); if (signInResult.status == FacebookLoginStatus.loggedIn) { LoginRepo.getInstance().signInWithFacebook(signInResult); } else if (signInResult.status == FacebookLoginStatus.cancelledByUser) { dispatch(LogoutEvent()); } else { dispatch(LoginErrorEvent(signInResult.errorMessage)); } }

두 번째 것은 어느 한 제공자로부터 프로필 데이터를 얻을 때 호출될 것입니다. 로그인 핸들러가 firebase_auth onAuthStateChange 스트림을 수신하도록 지시하여 이를 수행할 것입니다.

 void _setupAuthStateListener(LoginWidget view) { if (_authStateListener == null) { _authStateListener = FirebaseAuth.instance.onAuthStateChanged.listen((user) { if (user != null) { final loginProvider = user.providerId; UserRepo.getInstance().setCurrentUser(User.fromFirebaseUser(user)); if (loginProvider == "google") { // TODO analytics call for google login provider } else { // TODO analytics call for facebook login provider } view.navigateToMain(); } else { dispatch(LogoutEvent()); } }, onError: (error) { dispatch(LoginErrorEvent(error)); }); } }

UserRepoLoginRepo 구현은 여기에 게시되지 않지만 전체 참조를 위해 GitHub 리포지토리를 자유롭게 살펴보세요.

Flutter 튜토리얼: 인스턴트 메시징 앱을 빌드하는 방법

마지막으로 흥미로운 부분에 도달합니다. 이름에서 알 수 있듯이 메시지는 가능한 한 빨리 교환되어야 하며 이상적으로는 즉시 교환되어야 합니다. 운 좋게도 cloud_firestore 를 사용하면 Firestore 인스턴스와 상호 작용할 수 있고 해당 snapshots() 기능을 사용하여 실시간 업데이트를 제공할 데이터 스트림을 열 수 있습니다. 내 생각에 모든 chat_repo 코드는 startChatroomForUsers 메서드를 제외하고 매우 간단합니다. 두 사용자를 모두 포함하는 기존 대화방이 없는 경우(동일한 사용자 쌍의 여러 인스턴스를 원하지 않기 때문에) 두 사용자를 위한 새 대화방을 만드는 책임이 있습니다. 이 경우 기존 대화방을 반환합니다.

그러나 Firestore의 설계로 인해 현재 중첩된 array-contains 쿼리를 지원하지 않습니다. 따라서 적절한 데이터 스트림을 검색할 수 없지만 당사 측에서 추가 필터링을 수행해야 합니다. 이 솔루션은 로그인한 사용자의 모든 대화방을 검색한 다음 선택한 사용자가 포함된 대화방을 검색하는 것으로 구성됩니다.

 Future<SelectedChatroom> startChatroomForUsers(List<User> users) async { DocumentReference userRef = _firestore .collection(FirestorePaths.USERS_COLLECTION) .document(users[1].uid); QuerySnapshot queryResults = await _firestore .collection(FirestorePaths.CHATROOMS_COLLECTION) .where("participants", arrayContains: userRef) .getDocuments(); DocumentReference otherUserRef = _firestore .collection(FirestorePaths.USERS_COLLECTION) .document(users[0].uid); DocumentSnapshot roomSnapshot = queryResults.documents.firstWhere((room) { return room.data["participants"].contains(otherUserRef); }, orElse: () => null); if (roomSnapshot != null) { return SelectedChatroom(roomSnapshot.documentID, users[0].displayName); } else { Map<String, dynamic> chatroomMap = Map<String, dynamic>(); chatroomMap["messages"] = List<String>(0); List<DocumentReference> participants = List<DocumentReference>(2); participants[0] = otherUserRef; participants[1] = userRef; chatroomMap["participants"] = participants; DocumentReference reference = await _firestore .collection(FirestorePaths.CHATROOMS_COLLECTION) .add(chatroomMap); DocumentSnapshot chatroomSnapshot = await reference.get(); return SelectedChatroom(chatroomSnapshot.documentID, users[0].displayName); } }

또한 유사한 설계 제약으로 인해 Firebase는 현재 특수 FieldValue.serverTimestamp() 값을 사용하여 배열 업데이트(기존 배열 필드 값에 새 요소 삽입)를 지원하지 않습니다.

이 값은 실제 값 대신 이 값을 포함하는 필드가 트랜잭션이 발생하는 순간 서버의 실제 타임스탬프로 채워져야 함을 플랫폼에 나타냅니다. 대신, 우리는 새로운 메시지 직렬화 객체를 생성하고 해당 객체를 채팅방 메시지 컬렉션에 삽입하는 순간에 DateTime.now() 를 사용하고 있습니다.

 Future<bool> sendMessageToChatroom(String chatroomId, User user, String message) async { try { DocumentReference authorRef = _firestore.collection(FirestorePaths.USERS_COLLECTION).document(user.uid); DocumentReference chatroomRef = _firestore.collection(FirestorePaths.CHATROOMS_COLLECTION).document(chatroomId); Map<String, dynamic> serializedMessage = { "author" : authorRef, "timestamp" : DateTime.now(), "value" : message }; chatroomRef.updateData({ "messages" : FieldValue.arrayUnion([serializedMessage]) }); return true; } catch (e) { print(e.toString()); return false; } }

마무리

분명히 우리가 개발한 Flutter 메시징 앱은 시장에 출시된 인스턴트 메시징 애플리케이션보다 개념 증명에 가깝습니다. 추가 개발을 위한 아이디어로 종단 간 암호화 또는 풍부한 콘텐츠(그룹 채팅, 미디어 첨부, URL 구문 분석) 도입을 고려할 수 있습니다. 그러나 그 전에 푸시 알림은 인스턴트 메시징 응용 프로그램의 필수 기능이기 때문에 구현해야 하며 간결함을 위해 이 기사의 범위에서 제외했습니다. 또한 Firestore에는 중첩 array-contains 쿼리와 같은 더 간단하고 정확한 데이터를 제공하기 위해 몇 가지 기능이 여전히 누락되어 있습니다.

기사 시작 부분에서 언급했듯이 Flutter는 최근에야 안정적인 1.0 릴리스로 성숙했으며 프레임워크 기능 및 기능뿐 아니라 개발 커뮤니티 및 타사 라이브러리 및 자원. 지금 Flutter 앱 개발에 익숙해지는 데 시간을 투자하는 것이 합리적입니다. Flutter 앱 개발은 분명히 모바일 개발 프로세스를 유지하고 가속화하기 위해 존재하기 때문입니다.

추가 비용 없이 Flutter 개발자는 Google의 새로운 OS인 Fuchsia를 대상으로 할 준비도 되어 있습니다.

관련: Dart 언어: Java 및 C#이 충분히 날카롭지 않을 때