Flutterチュートリアル:最初のFlutterアプリを作成する方法

公開: 2022-03-11

Flutterとは何ですか?

FlutterはGoogleのモバイルアプリ開発SDKであり、2つの別々のコードベースを維持することなく、AndroidプラットフォームとiOSプラットフォームの両方を同時にターゲットにすることができます。 さらに、Flutterを使用するアプリは、Googleの今後のFuchsiaオペレーティングシステムをターゲットにするようにコンパイルすることもできます。

Flutterは最近、主要なマイルストーンである安定バージョン1.0に到達しました。 リリースは、2018年12月5日にロンドンのFlutterLiveイベントで行われました。 それはまだ初期の進化するソフトウェアベンチャーと見なすことができますが、この記事では、すでに実証済みの概念に焦点を当て、Flutter1.2とFirebaseを使用して両方の主要なモバイルプラットフォームを対象とする完全に機能するメッセージングアプリを開発する方法を示します。

下のグラフからわかるように、Flutterはここ数か月で多くのユーザーを獲得しています。 2018年、Flutterの市場シェアは2倍になり、検索クエリの点でReact Nativeを上回る軌道に乗っているため、新しいFlutterチュートリアルを作成することにしました。

2018年7月から9月までのFlutterユーザーとReactユーザーを比較したグラフ。

注:この記事では、実装の特定の部分にのみ焦点を当てています。 プロジェクトの完全なソースコードリファレンスは、このGitHubリポジトリにあります。

前提条件

モバイル開発の最初の試みであっても、読者がこのプロジェクトをフォローして達成できるように努力しましたが、Flutter固有ではない多くのコアモバイル開発の概念が言及され、詳細な説明なしで使用されています。

その目的の1つは読者が一度にプロジェクトを完了することであるため、これは記事の簡潔さのために行われています。 最後に、この記事では、必要なAndroidStudioプラグインやFlutterSDKなど、開発環境が既にセットアップされていることを前提としています。

Firebaseのセットアップ

プラットフォームごとに個別に行う必要があるのは、Firebaseの設定だけです。 まず、Firebase Dashboardで新しいプロジェクトを作成し、新しく生成されたワークスペースにAndroidおよびiOSアプリケーションを追加してください。 プラットフォームは、ダウンロードする必要のある2つの構成ファイルを生成します。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:

最初の2つはFirebaseとは関係ありませんが、プロジェクトで頻繁に使用されます。 最後の2つは、うまくいけば、自明です。

最後に、認証フローを正常に完了することができるように、プラットフォーム固有のプロジェクト設定を構成する必要があります。 Android側では、プロジェクトレベルのGradle構成にgoogle-servicesGradleプラグインを追加する必要があります。 つまり、 $(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アプリケーションのパラメーターを登録することです。 ここで探しているのは、これら2つのファイルを編集することです- $(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の時間です。 幸い、この場合、変更する必要があるのは1つのファイルだけです。 次の値を$(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アーキテクチャに関する一言

このアーキテクチャ標準は、以前の記事の1つで説明されており、FlutterとAngularDartでのコード共有にBLoCを使用する方法を示しているため、ここでは詳しく説明しません。

主なアイデアの背後にある基本的な考え方は、すべての画面に次のクラスがあるということです。- view-現在の状態を表示し、ユーザー入力をイベントとしてブロックに委任する役割を果たします。 -状態-これは、ユーザーが現在のビューを使用して操作する「ライブ」データを表します。 --bloc-イベントに応答し、それに応じて状態を更新します。オプションで、1つまたは複数のローカルまたはリモートリポジトリからデータを要求します。 -イベント-これは、現在の状態を変更する場合と変更しない場合がある明確なアクション結果です。

グラフィック表現として、次のように考えることができます。

Flutterチュートリアル:BLoCアーキテクチャのグラフィック表現。

さらに、これらのクラスのインスタンスを生成するデータクラスとリポジトリを含むモデルディレクトリがあります。

UI開発

FLutterを使用したUIの作成は、UIがXMLスキームを使用して構築され、ビジネスロジックコードベースから完全に分離されているAndroidおよびiOSでのネイティブアプリ開発とは対照的に、Dartで完全に行われます。 現在の状態(isLoading、isEmptyパラメーターなど)に基づいて、さまざまなコンポーネントを使用した比較的単純なUI要素の構成を使用します。 FlutterのUIは、ウィジェット、つまりウィジェットツリーを中心に展開します。 ウィジェットは、ステートレスまたはステートフルのいずれかになります。 ステートフルなものに関しては、現在表示されている特定のウィジェットでsetState()が呼び出されると(コンストラクターで呼び出すか、破棄された後にランタイムエラーが発生する)、ビルドと描画のパスが次の描画サイクルで実行されるようにスケジュールされています。

簡潔にするために、ここではUI(ビュー)クラスの1つのみを示します。

 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 、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つの独立した部分があります。 1つ目は、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)); } }

2つ目は、いずれかのプロバイダーからプロファイルデータを取得するときに呼び出されます。 これを実現するには、ログインハンドラーに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()機能を使用して、リアルタイムで更新を提供するデータストリームを開くことができます。 私の意見では、 startChatroomForUsersメソッドを除いて、すべてのchat_repoコードは非常に単純です。 両方のユーザーを含む既存のチャットルームがない限り(同じユーザーペアの複数のインスタンスを持ちたくないため)、2人のユーザー用の新しいチャットルームを作成する責任があります。その場合、既存のチャットルームが返されます。

ただし、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解析)の導入を検討することができます。 ただし、その前に、プッシュ通知はインスタントメッセージングアプリケーションに必須の機能であるため、実装する必要があります。簡潔にするために、プッシュ通知はこの記事の範囲外に移動しました。 さらに、ネストされたarray-containsクエリを作成するために、Firestoreにはまだいくつかの機能がありません。

記事の冒頭で述べたように、Flutterは最近安定した1.0リリースに成熟し、フレームワークの機能だけでなく、開発コミュニティやサードパーティのライブラリや資力。 モバイル開発プロセスを継続してスピードアップすることが明らかにここにあるので、今すぐFlutterアプリ開発に精通することに時間を費やすことは理にかなっています。

追加費用なしで、Flutter開発者はGoogleの新しいOSであるFuchsiaをターゲットにする準備もできています。

関連: Dart言語:JavaとC#が十分にシャープでない場合