บทช่วยสอน Flutter: วิธีสร้างแอพ Flutter แรกของคุณ
เผยแพร่แล้ว: 2022-03-11Flutter คืออะไร?
Flutter คือ SDK การพัฒนาแอปบนอุปกรณ์เคลื่อนที่ของ Google ที่ช่วยให้ผลิตภัณฑ์ของคุณกำหนดเป้าหมายทั้งแพลตฟอร์ม Android และ iOS ได้พร้อมกัน โดยไม่จำเป็นต้องดูแลรักษาฐานโค้ดสองชุดแยกกัน นอกจากนี้ แอปที่ใช้ Flutter ยังสามารถรวบรวมเพื่อกำหนดเป้าหมายระบบปฏิบัติการ Fuchsia ที่กำลังจะมีขึ้นของ Google ได้อีกด้วย
Flutter เพิ่งบรรลุเป้าหมายสำคัญ - เวอร์ชันเสถียร 1.0 การเปิดตัวเกิดขึ้นในลอนดอน 5 ธันวาคม 2018 ที่งาน Flutter Live แม้ว่าจะยังคงถือได้ว่าเป็นการลงทุนด้านซอฟต์แวร์ในช่วงต้นและมีการพัฒนา แต่บทความนี้จะเน้นที่แนวคิดที่ได้รับการพิสูจน์แล้ว และสาธิตวิธีการพัฒนาแอปรับส่งข้อความที่ทำงานได้อย่างสมบูรณ์ซึ่งกำหนดเป้าหมายไปยังทั้งแพลตฟอร์มมือถือหลักที่ใช้ Flutter 1.2 และ Firebase
ดังที่เห็นได้จากแผนภูมิด้านล่าง Flutter ได้รับผู้ใช้จำนวนมากในช่วงไม่กี่เดือนที่ผ่านมา ในปี 2018 ส่วนแบ่งการตลาดของ Flutter เพิ่มขึ้นเป็นสองเท่าและกำลังจะแซงหน้า React Native ในแง่ของคำค้นหา ดังนั้นเราจึงตัดสินใจสร้างบทช่วยสอน Flutter ใหม่
หมายเหตุ: บทความนี้เน้นเฉพาะบางส่วนของการใช้งานเท่านั้น การอ้างอิงซอร์สโค้ดแบบเต็มสำหรับโครงการมีอยู่ใน repo GitHub นี้
ข้อกำหนดเบื้องต้น
แม้ว่าจะมีความพยายามเพื่อให้ผู้อ่านสามารถติดตามและบรรลุโครงการนี้ แม้ว่าจะเป็นครั้งแรกที่พวกเขาพยายามพัฒนาอุปกรณ์พกพา แต่ก็มีการกล่าวถึงและใช้แนวคิดหลักในการพัฒนาอุปกรณ์พกพาจำนวนมากที่ไม่เฉพาะ Flutter โดยไม่มีคำอธิบายโดยละเอียด
มีการดำเนินการเพื่อความกระชับของบทความเนื่องจากหนึ่งในวัตถุประสงค์คือเพื่อให้ผู้อ่านทำโครงการให้เสร็จในคราวเดียว สุดท้าย บทความนี้จะถือว่าคุณมีสภาพแวดล้อมการพัฒนาอยู่แล้ว รวมถึงปลั๊กอิน Android Studio ที่จำเป็นและ Flutter SDK
ตั้งค่า Firebase
การตั้งค่า Firebase เป็นสิ่งเดียวที่เราต้องทำแยกกันสำหรับแต่ละแพลตฟอร์ม ก่อนอื่น ตรวจสอบให้แน่ใจว่าคุณสร้างโปรเจ็กต์ใหม่ใน Firebase Dashboard และเพิ่มแอปพลิเคชัน Android และ iOS ในพื้นที่ทำงานที่สร้างขึ้นใหม่ แพลตฟอร์มจะสร้างไฟล์การกำหนดค่าสองไฟล์ที่คุณต้องดาวน์โหลด ได้แก่ google-services.json สำหรับ Android และ GoogleService-Info.plist สำหรับ iOS ก่อนปิดแดชบอร์ด ตรวจสอบให้แน่ใจว่าได้เปิดใช้งานผู้ให้บริการตรวจสอบสิทธิ์ Firebase และ Google เนื่องจากเราจะใช้สำหรับระบุตัวตนผู้ใช้ ในการดำเนินการนี้ ให้เลือกรายการการรับรองความถูกต้องจากเมนูแล้วเลือกแท็บวิธีการลงชื่อเข้าใช้
ตอนนี้คุณสามารถปิดแดชบอร์ดได้เนื่องจากการตั้งค่าที่เหลือเกิดขึ้นใน codebase ของเรา ก่อนอื่น เราต้องใส่ไฟล์ที่เราดาวน์โหลดมาไว้ในโปรเจ็กต์ของเรา ไฟล์ google-services.json ควรอยู่ในโฟลเดอร์ $(FLUTTER_PROJECT_ROOT)/android/app และ GoogleService-Info.plist ควรอยู่ในไดเรกทอรี $(FLUTTER_PROJECT_ROOT)/ios/Runner ต่อไป เราต้องตั้งค่าไลบรารี Firebase จริง ๆ ที่เราจะใช้ในโปรเจ็กต์และเชื่อมต่อกับไฟล์การกำหนดค่า ทำได้โดยการระบุแพ็คเกจ Dart (ไลบรารี) ที่เราจะใช้ในไฟล์ pubspec.yaml ของโปรเจ็กต์ ในส่วนการพึ่งพาของไฟล์ ให้วางข้อมูลโค้ดต่อไปนี้:
flutter_bloc: shared_preferences: firebase_auth: cloud_firestore: google_sign_in: flutter_facebook_login:สองรายการแรกไม่เกี่ยวข้องกับ Firebase แต่จะใช้บ่อยในโปรเจ็กต์ สองข้อสุดท้ายหวังว่าจะอธิบายตนเองได้
สุดท้าย เราต้องกำหนดการตั้งค่าโปรเจ็กต์เฉพาะแพลตฟอร์ม ซึ่งจะทำให้ขั้นตอนการพิสูจน์ตัวตนของเราเสร็จสมบูรณ์ได้สำเร็จ ในด้าน Android เราจำเป็นต้องเพิ่มปลั๊กอิน Gradle ของบริการ Google ให้กับการกำหนดค่า 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 โชคดีที่เราจำเป็นต้องเปลี่ยนไฟล์เดียวเท่านั้นในกรณีนี้ เพิ่มค่าต่อไปนี้ (โปรดทราบว่ารายการ CFBundleURLTypes อาจมีอยู่แล้วในรายการ ในกรณีนี้ คุณต้องเพิ่มรายการเหล่านี้ในอาร์เรย์ที่มีอยู่แทนที่จะประกาศอีกครั้ง) ไปที่ $(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist ไฟล์:
<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
มาตรฐานสถาปัตยกรรมนี้มีอธิบายไว้ในบทความก่อนหน้าของเรา ซึ่งแสดงให้เห็นถึงการใช้ BLoC สำหรับการแชร์โค้ดใน Flutter และ AngularDart ดังนั้นเราจะไม่อธิบายโดยละเอียดที่นี่
แนวคิดพื้นฐานที่อยู่เบื้องหลังแนวคิดหลักคือทุกหน้าจอมีคลาสต่อไปนี้: - มุมมอง - ซึ่งมีหน้าที่แสดงสถานะปัจจุบันและมอบหมายอินพุตของผู้ใช้เป็นเหตุการณ์ไปยังกลุ่ม - รัฐ - ซึ่งแสดงถึงข้อมูล "สด" ที่ผู้ใช้โต้ตอบด้วยโดยใช้มุมมองปัจจุบัน - บล็อก - ซึ่งตอบสนองต่อเหตุการณ์และอัปเดตสถานะตามลำดับ โดยเลือกที่จะขอข้อมูลจากที่เก็บข้อมูลในเครื่องหรือระยะไกลหนึ่งแห่งหรือหลายรายการ - เหตุการณ์ - ซึ่งเป็นผลการดำเนินการที่แน่นอนที่อาจหรือไม่อาจเปลี่ยนสถานะปัจจุบัน
สำหรับการแสดงภาพกราฟิก สามารถคิดได้ดังนี้:
นอกจากนี้ เรามีไดเร็กทอรี model ซึ่งมีคลาสข้อมูลและที่เก็บที่สร้างอินสแตนซ์ของคลาสเหล่านี้
การพัฒนา UI
การสร้าง UI โดยใช้ Flutter ทำได้อย่างสมบูรณ์ใน Dart ซึ่งต่างจากการพัฒนาแอปแบบเนทีฟใน Android และ iOS ที่ UI สร้างขึ้นโดยใช้รูปแบบ XML และแยกออกจากโค้ดเบสตรรกะทางธุรกิจโดยสิ้นเชิง เราจะใช้องค์ประกอบองค์ประกอบ UI ที่ค่อนข้างง่ายพร้อมองค์ประกอบที่แตกต่างกันตามสถานะปัจจุบัน (เช่น isLoading, พารามิเตอร์ isEmpty) UI ใน Flutter หมุนรอบวิดเจ็ต หรือมากกว่าแผนผังวิดเจ็ต วิดเจ็ตอาจเป็นแบบไร้สถานะหรือแบบเก็บสถานะก็ได้ เมื่อพูดถึง stateful สิ่งสำคัญคือต้องเน้นว่าเมื่อมีการเรียกใช้ setState() บนวิดเจ็ตเฉพาะที่แสดงอยู่ในปัจจุบัน (เรียกใน Constructor หรือหลังจากที่กำจัดทิ้งส่งผลให้เกิดข้อผิดพลาดรันไทม์) build and draw pass คือ กำหนดให้ดำเนินการในรอบการวาดภาพถัดไป
เพื่อความกระชับ เราจะแสดงหนึ่งในคลาส 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_in และ flutter_facebook_login เพื่อตรวจสอบสิทธิ์ผู้ใช้โดยอาศัยโปรไฟล์โซเชียลเน็ตเวิร์กของพวกเขา ก่อนอื่น ตรวจสอบให้แน่ใจว่าได้นำเข้าแพ็คเกจเหล่านี้ไปยังไฟล์ที่จะจัดการกับตรรกะคำขอเข้าสู่ระบบ:
import 'package:flutter_facebook_login/flutter_facebook_login.dart'; import 'package:google_sign_in/google_sign_in.dart';ตอนนี้ เราจะมี 2 ส่วนอิสระที่จะดูแลขั้นตอนการรับรองความถูกต้องของเรา คนแรกกำลังจะเริ่มต้นคำขอลงชื่อเข้าใช้ 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 stream:
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)); }); } } การใช้งาน UserRepo และ LoginRepo จะไม่ถูกโพสต์ที่นี่ แต่โปรดดูที่ GitHub repo เพื่อเป็นข้อมูลอ้างอิงทั้งหมด
บทช่วยสอน 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 ที่เราพัฒนาขึ้นนั้นเป็นแนวความคิดที่พิสูจน์ได้มากกว่าแอปพลิเคชันการส่งข้อความโต้ตอบแบบทันทีที่พร้อมใช้งานในตลาด เพื่อเป็นแนวคิดในการพัฒนาต่อไป เราอาจพิจารณาแนะนำการเข้ารหัสแบบ end-to-end หรือเนื้อหาที่หลากหลาย (การแชทเป็นกลุ่ม ไฟล์แนบของสื่อ การแยกวิเคราะห์ URL) แต่ก่อนหน้านั้น เราควรใช้การแจ้งเตือนแบบพุช เนื่องจากเป็นคุณสมบัติที่ต้องมีสำหรับแอปพลิเคชันการส่งข้อความโต้ตอบแบบทันที และเราได้ย้ายออกจากขอบเขตของบทความนี้เพื่อความกระชับ นอกจากนี้ Firestore ยังขาดคุณสมบัติสองสามอย่างเพื่อให้มีการสืบค้นข้อมูล array-contains ซ้อนกันเหมือนข้อมูลที่เรียบง่ายและแม่นยำยิ่งขึ้น
ดังที่กล่าวไว้ในตอนต้นของบทความ Flutter เพิ่งเติบโตเป็นรุ่น 1.0 ที่เสถียรและจะเติบโตอย่างต่อเนื่อง ไม่เพียงแต่เมื่อพูดถึงฟีเจอร์และความสามารถของเฟรมเวิร์กเท่านั้น แต่ยังรวมถึงชุมชนการพัฒนาและไลบรารีของบุคคลที่สามด้วย ทรัพยากร. คุณควรสละเวลาเพื่อทำความคุ้นเคยกับการพัฒนาแอป Flutter ในขณะนี้ เนื่องจากเห็นได้ชัดว่าอยู่ที่นี่เพื่ออยู่ให้และเร่งกระบวนการพัฒนาอุปกรณ์เคลื่อนที่ของคุณ
โดยไม่มีค่าใช้จ่ายเพิ่มเติม นักพัฒนา Flutter ก็พร้อมที่จะกำหนดเป้าหมาย OS–Fuchsia ที่เกิดขึ้นใหม่ของ Google
