방명록
- [Flutter] "Do it! 플러터 앱 프로그래밍" - 오픈 API를 활용한 여행 정보 앱 프로젝트 | 파이어베이스 설정, 로인인/회원가입 기능2022년 02월 26일 23시 25분 12초에 업로드 된 글입니다.작성자: DandyNow728x90반응형
"조준수. (2021). Do it! 플러터 앱 프로그래밍. 이지스퍼블리싱", 14장 오픈 API를 활용한 여행 정보 앱 만들기를 실습했다. 파이어베이스 설정부터 로인인/회원가입 기능까지 구현하였다. 이 과정에서 package name 불일치, multidex 문제가 있어 해결하였다.
여행 정보 앱 프로젝트 시작
프로젝트 만들기
패키지 이름은 구글 플레이에서 고유하다. 따라서 기존에 등록된 패키지명과 중복되면 앱을 등록할 수 없다. 한 번 정하면 앱 출시 후 변경할 수 없다.
com.회사명(또는 닉네임).프로젝트명
파이어베이스 설정하기
- 파이어베이스 콘솔에서 새 프로젝트 만들기
- 새 프로젝트에 안드로이드 앱 추가하기
- 플러터 프로젝트 생성 시 지정했던 패키지 이름 사용
- google-services.json 내려받아서 안드로이드 프로젝트의 app 폴더에 넣기
- 안드로이드 프로젝트에서 android/build.gradle에 클래스 경로 추가하기
- 안드로이드 프로젝트에서 android/app/build.gradle에 구글 서비스 플러그인 추가하기
- 실시간 데이터베이스 추가하기
- 읽기와 쓰기 규칙 true로 수정하기
- 데이터베이스 URL 복사해 놓기
- 애드몹에 안드로이드 앱 추가하기
- 앱 ID 복사해 놓기
- 안드로이드 프로젝트에서 AndroidManifest.xml 파일에 메타 데이터 추가하기(앱 ID 이용)
인트로와 로그인 화면 만들기
패키지 버전을 생략하면 최신 버전으로 내려받는다. 하지만 API가 변경되었을 경우 대응하기 어렵고 안정화되지 않아서 버그가 있을 수 있다. 따라서 검증된 버전을 명시해서 사용하는 것이 좋다.
# pubspec.yaml dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 # 이하 추가 firebase_core: ^1.3.0 firebase_analytics: ^8.1.2 firebase_database: ^7.1.1 firebase_messaging: ^10.0.1 google_mobile_ads: ^0.13.0 http: ^0.13.1 sqflite: ^2.0.0+3 path: ^1.8.0 shared_preferences: ^2.0.5 crypto: ^3.0.1 google_maps_flutter: ^2.0.3
// main.dart import 'package:flutter/material.dart'; import 'signPage.dart'; import 'login.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); MobileAds.instance.initialize(); runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: '부버의 여행', theme: ThemeData( primarySwatch: Colors.blue, ), initialRoute: '/', routes: { '/': (context) => LoginPage(), '/sign': (context) => SignPage(), }, ); } }
// login.dart import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/material.dart'; import 'data/user.dart'; // 로그인 화면 만들기 class LoginPage extends StatefulWidget { @override State<StatefulWidget> createState() => _LoginPage(); } class _LoginPage extends State<LoginPage> with SingleTickerProviderStateMixin { FirebaseDatabase? _database; DatabaseReference? reference; String _databaseURL = '###파이어베이스 실시간 데이터베이스 URL###'; double opacity = 0; AnimationController? _animationController; Animation? _animation; TextEditingController? _idTextController; TextEditingController? _pwTextController; @override void initState() { super.initState(); _idTextController = TextEditingController(); _pwTextController = TextEditingController(); _animationController = AnimationController(duration: Duration(seconds: 3), vsync: this); _animation = Tween<double>(begin: 0, end: pi * 2).animate(_animationController!); _animationController!.repeat(); Timer(Duration(seconds: 2), () { setState(() { opacity = 1; // 페이지 생성 후 2초 후 타이머 시작 }); }); _database = FirebaseDatabase(databaseURL: _databaseURL); reference = _database?.reference().child('user'); } @override void dispose() { _animationController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Container( child: Center( child: Column( children: <Widget>[ AnimatedBuilder( animation: _animationController!, builder: (context, widget) { return Transform.rotate( angle: _animation?.value, child: widget, ); }, child: Icon( Icons.airplanemode_active, color: Colors.deepOrangeAccent, size: 80, ), ), SizedBox( height: 100, child: Center( child: Text( '모두의 여행', style: TextStyle(fontSize: 30), ), ), ), AnimatedOpacity( opacity: opacity, duration: Duration(seconds: 1), child: Column( children: <Widget>[ SizedBox( width: 200, child: TextField( controller: _idTextController, maxLines: 1, decoration: InputDecoration( labelText: '아이디', border: OutlineInputBorder()), ), ), SizedBox( height: 20, ), SizedBox( width: 200, child: TextField( controller: _pwTextController, obscureText: true, maxLines: 1, decoration: InputDecoration( labelText: '비밀번호', border: OutlineInputBorder()), ), ), Row( children: <Widget>[ MaterialButton( onPressed: () { Navigator.of(context).pushNamed('/sign'); }, child: Text('회원가입')), MaterialButton( onPressed: () { if (_idTextController?.value.text.length == 0 || _pwTextController?.value.text.length == 0) { makeDialog('빈칸이 있습니다'); } else { reference! .child(_idTextController!.value.text) .onValue .listen((event) { if (event.snapshot.value == null) { makeDialog('아이디가 없습니다'); } else { reference! .child(_idTextController!.value.text) .onChildAdded .listen((event) { User user = User.fromSnapshot(event.snapshot); // 사용자가 입력한 비밀번호를 utf8로 인코딩한 후 sha1로 변경한다. // SHA-1은 해시 알고리즘을 이용해 특정 단어를 무작위 텍스트로 바꿔주는 함수이다. // 해시 알고리즘을 사용하면 문자열을 짧은 길이의 키로 변환하고 고속으로 검색할 수 있게 해준다. // 또한, 서버에 암호를 저장할 때 원문을 저장하지 않기 때문에 보안에도 좋다. var bytes = utf8.encode( _pwTextController!.value.text); var digest = sha1.convert(bytes); // --------------------------------- if (user.pw == digest.toString()) { Navigator.of(context) .pushReplacementNamed('/main', arguments: _idTextController! .value.text); } else { makeDialog('비밀번호가 틀립니다'); } }); } }); } }, child: Text('로그인')) ], mainAxisAlignment: MainAxisAlignment.center, ) ], ), ) ], mainAxisAlignment: MainAxisAlignment.center, ), ), ), ); } void makeDialog(String text) { showDialog( context: context, builder: (context) { return AlertDialog( content: Text(text), ); }); } }
// signPage.dart import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/material.dart'; import 'data/user.dart'; class SignPage extends StatefulWidget { @override State<StatefulWidget> createState() => _SignPage(); } class _SignPage extends State<SignPage> { FirebaseDatabase? _database; DatabaseReference? reference; String _databaseURL = '###파이어베이스 실시간 데이터베이스 URL###'; TextEditingController? _idTextController; TextEditingController? _pwTextController; TextEditingController? _pwCheckTextController; @override void initState() { super.initState(); _idTextController = TextEditingController(); _pwTextController = TextEditingController(); _pwCheckTextController = TextEditingController(); _database = FirebaseDatabase(databaseURL: _databaseURL); reference = _database?.reference().child('user'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('회원가입'), ), body: Container( child: Center( child: Column( children: <Widget>[ SizedBox( width: 200, child: TextField( controller: _idTextController, maxLines: 1, decoration: InputDecoration( hintText: '4자 이상 입력해주세요', labelText: '아이디', border: OutlineInputBorder()), ), ), SizedBox( height: 20, ), SizedBox( width: 200, child: TextField( controller: _pwTextController, obscureText: true, maxLines: 1, decoration: InputDecoration( hintText: '6자 이상 입력해주세요', labelText: '비밀번호', border: OutlineInputBorder()), ), ), SizedBox( height: 20, ), SizedBox( width: 200, child: TextField( controller: _pwCheckTextController, obscureText: true, maxLines: 1, decoration: InputDecoration( labelText: '비밀번호확인', border: OutlineInputBorder()), ), ), SizedBox( height: 20, ), MaterialButton( onPressed: () { if (_idTextController!.value.text.length >= 4 && _pwTextController!.value.text.length >= 6) { if (_pwTextController!.value.text == _pwCheckTextController!.value.text) { var bytes = utf8.encode(_pwTextController!.value.text); var digest = sha1.convert(bytes); reference! .child(_idTextController!.value.text) .push() .set(User( _idTextController!.value.text, digest.toString(), DateTime.now().toIso8601String()) .toJson()) .then((_) { Navigator.of(context).pop(); }); } else { makeDialog('비밀번호가 틀립니다'); } } else { makeDialog('길이가 짧습니다'); } }, child: Text( '회원가입', style: TextStyle(color: Colors.white), ), color: Colors.blueAccent, ) ], mainAxisAlignment: MainAxisAlignment.center, ), ), ), ); } void makeDialog(String text) { showDialog( context: context, builder: (context) { return AlertDialog( content: Text(text), ); }); } }
// data/user.dart import 'package:firebase_database/firebase_database.dart'; // 데이터 구조 만들기 class User { String id; String pw; String createTime; User(this.id, this.pw, this.createTime); User.fromSnapshot(DataSnapshot snapshot) : id = snapshot.value['id'], pw = snapshot.value['pw'], createTime = snapshot.value['createTime']; toJson() { return { 'id': id, 'pw': pw, 'createTime': createTime, }; } }
간편 로그인 기능
유명한 서비스에 가입된 계정으로 로그인할 수 있는데 firebase_auth 패키지를 이용하면 된다.
에러 해결
package name 불일치 문제
> No matching client found for package name 'com.example.flutter_modu_tour'
위 에러에 대하여 google-services.json, build.gradle을 아래와 같이 각각 수정하여 해결했다.
// android/app/google-services.json "android_client_info": { "package_name": "com.sewol.buber_tour" }
// android/app/build.gradle defaultConfig { applicationId "com.sewol.buber_tour"
multidex 문제
Flutter multidex handling is disabled. If you wish to let the tool configure multidex, use the │ --mutidex flag.
위 에러에 대하여 build.gradle을 아래와 같이 수정하여 해결했다.
// android/app/build.gradle defaultConfig { applicationId "com.sewol.buber_tour" minSdkVersion 20 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName multiDexEnabled true // 추가 } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation platform('com.google.firebase:firebase-bom:29.1.0') implementation 'com.google.firebase:firebase-analytics-ktx' implementation 'com.android.support:multidex:1.0.3' // 추가 }
728x90반응형'언어·프레임워크 > Flutter' 카테고리의 다른 글
다음글이 없습니다.이전글이 없습니다.댓글