Open17
Flutter + AppWriteでGoogle認証を実装してみる

プロジェクトの新規作成
flutter create appwrite_google
flutter run
VSCodeでの実行
Flutter拡張機能をinstall
- cmd + shift + p
- flutter:Select Device
- Start iOS

f5を押して起動.

AppWriteなどのpackageを入れる
flutter pub add appwrite
flutter pub add flutter_svg
flutter pub add provider
pubspec.yaml
assets:
- assets/google_icon.svg
- assets/apple_icon.svg
- assets/github_icon.svg
- assets/twitter_icon.svg

変数
lib/contants/constants.dart
const String APPWRITE_PROJECT_ID = "";
const String APPWRITE_DATABASE_ID = "";
const String APPWRITE_URL = "https://cloud.appwrite.io/v1";
const String COLLECTION_MESSAGES = "";

auth_api
lib/appwrite/auth_api.dart
import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';
import 'package:flutter/widgets.dart';
import '../contants/constants.dart';
enum AuthStatus {
uninitialized,
authenticated,
unauthenticated,
}
class AuthAPI extends ChangeNotifier {
Client client = Client();
late final Account account;
late User _currentUser;
AuthStatus _status = AuthStatus.uninitialized;
// Getter methods
User get currentUser => _currentUser;
AuthStatus get status => _status;
String? get username => _currentUser?.name;
String? get email => _currentUser?.email;
String? get userid => _currentUser?.$id;
// Constructor
AuthAPI() {
init();
loadUser();
}
// Initialize the Appwrite client
init() {
client
.setEndpoint(APPWRITE_URL)
.setProject(APPWRITE_PROJECT_ID)
.setSelfSigned();
account = Account(client);
}
loadUser() async {
try {
final user = await account.get();
_status = AuthStatus.authenticated;
_currentUser = user;
} catch (e) {
_status = AuthStatus.unauthenticated;
} finally {
notifyListeners();
}
}
Future<User> createUser(
{required String email, required String password}) async {
notifyListeners();
try {
final user = await account.create(
userId: ID.unique(),
email: email,
password: password,
name: 'Simon G');
return user;
} finally {
notifyListeners();
}
}
Future<Session> createEmailSession(
{required String email, required String password}) async {
notifyListeners();
try {
final session =
await account.createEmailSession(email: email, password: password);
_currentUser = await account.get();
_status = AuthStatus.authenticated;
return session;
} finally {
notifyListeners();
}
}
signInWithProvider({required String provider}) async {
try {
final session = await account.createOAuth2Session(provider: provider);
_currentUser = await account.get();
_status = AuthStatus.authenticated;
return session;
} finally {
notifyListeners();
}
}
signOut() async {
try {
await account.deleteSession(sessionId: 'current');
_status = AuthStatus.unauthenticated;
} finally {
notifyListeners();
}
}
Future<Preferences> getUserPreferences() async {
return await account.getPrefs();
}
updatePreferences({required String bio}) async {
return account.updatePrefs(prefs: {'bio': bio});
}
}

UIの作成
lib/pages/login_page.dart
lib/pages/register_page.dart
lib/pages/tabs_page.dart
lib/pages/account_page.dart
lib/pages/messages_page.dart
lib/main.dart
import 'package:appwrite_google/appwrite/auth_api.dart';
import 'package:appwrite_google/pages/login_page.dart';
import 'package:appwrite_google/pages/tabs_page.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => AuthAPI(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
final value = context.watch<AuthAPI>().status;
print('TOP CHANGE Value changed to: $value!');
return MaterialApp(
title: 'Appwrite Auth Demo',
debugShowCheckedModeBanner: false,
home: value == AuthStatus.uninitialized
? const Scaffold(
body: Center(child: CircularProgressIndicator()),
)
: value == AuthStatus.authenticated
? const TabsPage()
: const LoginPage(),
theme: ThemeData(
colorScheme: ColorScheme.fromSwatch().copyWith(
primary: const Color(0xFFE91052),
),
));
}
}

login
import 'package:appwrite/appwrite.dart';
import 'package:appwrite_google/appwrite/auth_api.dart';
import 'package:appwrite_google/pages/messages_page.dart';
import 'package:appwrite_google/pages/register_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final emailTextController = TextEditingController();
final passwordTextController = TextEditingController();
bool loading = false;
signIn() async {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: const [
CircularProgressIndicator(),
]),
);
});
try {
final AuthAPI appwrite = context.read<AuthAPI>();
await appwrite.createEmailSession(
email: emailTextController.text,
password: passwordTextController.text,
);
Navigator.pop(context);
} on AppwriteException catch (e) {
Navigator.pop(context);
showAlert(title: 'Login failed', text: e.message.toString());
}
}
showAlert({required String title, required String text}) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(title),
content: Text(text),
actions: [
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Ok'))
],
);
});
}
signInWithProvider(String provider) {
try {
context.read<AuthAPI>().signInWithProvider(provider: provider);
} on AppwriteException catch (e) {
showAlert(title: 'Login failed', text: e.message.toString());
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Appwrite App'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: emailTextController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: passwordTextController,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
obscureText: true,
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
signIn();
},
icon: const Icon(Icons.login),
label: const Text("Sign in"),
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RegisterPage()));
},
child: const Text('Create Account'),
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const MessagesPage()));
},
child: const Text('Read Messages as Guest'),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () => signInWithProvider('google'),
style: ElevatedButton.styleFrom(
foregroundColor: Colors.black,
backgroundColor: Colors.white),
child:
SvgPicture.asset('assets/google_icon.svg', width: 12),
),
ElevatedButton(
onPressed: () => signInWithProvider('apple'),
style: ElevatedButton.styleFrom(
foregroundColor: Colors.black,
backgroundColor: Colors.white),
child: SvgPicture.asset('assets/apple_icon.svg', width: 12),
),
ElevatedButton(
onPressed: () => signInWithProvider('github'),
style: ElevatedButton.styleFrom(
foregroundColor: Colors.black,
backgroundColor: Colors.white),
child:
SvgPicture.asset('assets/github_icon.svg', width: 12),
),
ElevatedButton(
onPressed: () => signInWithProvider('twitter'),
style: ElevatedButton.styleFrom(
foregroundColor: Colors.black,
backgroundColor: Colors.white),
child:
SvgPicture.asset('assets/twitter_icon.svg', width: 12),
)
],
),
],
),
),
),
);
}
}

register
lib/pages/login_page.dart
import 'package:appwrite/appwrite.dart';
import 'package:appwrite_app/appwrite/auth_api.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class RegisterPage extends StatefulWidget {
const RegisterPage({Key? key}) : super(key: key);
_RegisterPageState createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final emailTextController = TextEditingController();
final passwordTextController = TextEditingController();
createAccount() async {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: const [
CircularProgressIndicator(),
]),
);
});
try {
final AuthAPI appwrite = context.read<AuthAPI>();
await appwrite.createUser(
email: emailTextController.text,
password: passwordTextController.text,
);
Navigator.pop(context);
const snackbar = SnackBar(content: Text('Account created!'));
ScaffoldMessenger.of(context).showSnackBar(snackbar);
} on AppwriteException catch (e) {
Navigator.pop(context);
showAlert(title: 'Account creation failed', text: e.message.toString());
}
}
showAlert({required String title, required String text}) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(title),
content: Text(text),
actions: [
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Ok'))
],
);
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Create your account'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: emailTextController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: passwordTextController,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
obscureText: true,
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
createAccount();
},
icon: const Icon(Icons.app_registration),
label: const Text('Sign up'),
),
],
),
),
),
);
}
}

message page
import 'package:appwrite/appwrite.dart';
import 'package:appwrite_google/appwrite/auth_api.dart';
import 'package:appwrite_google/appwrite/database_api.dart';
import 'package:flutter/material.dart';
import 'package:appwrite/models.dart';
import 'package:provider/provider.dart';
class MessagesPage extends StatefulWidget {
const MessagesPage({Key? key}) : super(key: key);
_MessagesPageState createState() => _MessagesPageState();
}
class _MessagesPageState extends State<MessagesPage> {
final database = DatabaseAPI();
late List<Document>? messages = [];
TextEditingController messageTextController = TextEditingController();
AuthStatus authStatus = AuthStatus.uninitialized;
void initState() {
super.initState();
final AuthAPI appwrite = context.read<AuthAPI>();
authStatus = appwrite.status;
loadMessages();
}
loadMessages() async {
try {
final value = await database.getMessages();
setState(() {
messages = value.documents;
});
} catch (e) {
print(e);
}
}
addMessage() async {
try {
await database.addMessage(message: messageTextController.text);
const snackbar = SnackBar(content: Text('Message added!'));
ScaffoldMessenger.of(context).showSnackBar(snackbar);
messageTextController.clear();
loadMessages();
} on AppwriteException catch (e) {
showAlert(title: 'Error', text: e.message.toString());
}
}
deleteMessage(String id) async {
try {
await database.deleteMessage(id: id);
loadMessages();
} on AppwriteException catch (e) {
showAlert(title: 'Error', text: e.message.toString());
}
}
showAlert({required String title, required String text}) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(title),
content: Text(text),
actions: [
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Ok'))
],
);
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Messages'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
authStatus == AuthStatus.authenticated
? Row(
children: [
Expanded(
child: TextField(
controller: messageTextController,
decoration: const InputDecoration(
hintText: 'Type a message'),
),
),
const SizedBox(width: 10),
ElevatedButton.icon(
onPressed: () {
addMessage();
},
icon: const Icon(Icons.send),
label: const Text("Send")),
],
)
: const Center(),
const SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: messages?.length ?? 0,
itemBuilder: (context, index) {
final message = messages![index];
return Card(
child: ListTile(
title: Text(message.data['text']),
trailing: IconButton(
onPressed: () {
deleteMessage(message.$id);
},
icon: const Icon(Icons.delete)),
));
}),
)
]),
),
),
);
}
}

database_api.dartの作成
lib/appwrite/database_api.dart
import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';
import 'package:appwrite_google/appwrite/auth_api.dart';
import '../contants/constants.dart';
class DatabaseAPI {
Client client = Client();
late final Account account;
late final Databases databases;
final AuthAPI auth = AuthAPI();
DatabaseAPI() {
init();
}
init() {
client
.setEndpoint(APPWRITE_URL)
.setProject(APPWRITE_PROJECT_ID)
.setSelfSigned();
account = Account(client);
databases = Databases(client);
}
Future<DocumentList> getMessages() {
return databases.listDocuments(
databaseId: APPWRITE_DATABASE_ID,
collectionId: COLLECTION_MESSAGES,
);
}
Future<Document> addMessage({required String message}) {
return databases.createDocument(
databaseId: APPWRITE_DATABASE_ID,
collectionId: COLLECTION_MESSAGES,
documentId: ID.unique(),
data: {
'text': message,
'date': DateTime.now().toString(),
'user_id': auth.userid
});
}
Future<dynamic> deleteMessage({required String id}) {
return databases.deleteDocument(
databaseId: APPWRITE_DATABASE_ID,
collectionId: COLLECTION_MESSAGES,
documentId: id);
}
}

account_page
import 'package:flutter/material.dart';
import 'package:appwrite_google/appwrite/auth_api.dart';
import 'package:provider/provider.dart';
class AccountPage extends StatefulWidget {
const AccountPage({Key? key}) : super(key: key);
_AccountPageState createState() => _AccountPageState();
}
class _AccountPageState extends State<AccountPage> {
late String? email, username;
TextEditingController bioTextController = TextEditingController();
void initState() {
super.initState();
final AuthAPI appwrite = context.read<AuthAPI>();
email = appwrite.email;
username = appwrite.username;
appwrite.getUserPreferences().then((value) {
if (value.data.isNotEmpty) {
setState(() {
bioTextController.text = value.data['bio'];
});
}
});
}
signOut() {
final AuthAPI appwrite = context.read<AuthAPI>();
appwrite.signOut();
}
savePreferences() {
final AuthAPI appwrite = context.read<AuthAPI>();
appwrite.updatePreferences(bio: bioTextController.text);
const snackbar = SnackBar(content: Text('Preferences updated!'));
ScaffoldMessenger.of(context).showSnackBar(snackbar);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Account'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
signOut();
},
),
],
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Welcome back $username!',
style: Theme.of(context).textTheme.headlineSmall),
Text('$email'),
const SizedBox(height: 40),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(children: [
TextField(
controller: bioTextController,
decoration: const InputDecoration(
labelText: 'Your Bio',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextButton(
onPressed: () => savePreferences(),
child: const Text('Save Preferences'),
),
]),
),
)
],
)),
));
}
}

assetsの作成
各アイコンのsvgを作る

appwriteでAuthを追加する
- Googleの認証を追加する
- apiキーを追加

ログインする時にエラー
AppwriteException (AppwriteException: general_unknown_origin, Invalid Origin. Register your new client (com.example.appwriteGoogle) as a new iOS platform on your project console dashboard (403))
appwriteのプラットフォームを追加した時のurl?が違ったため。AppWriteの方を変更しておいた。Flutter側で変更しても良さそう。このドメイン、実際に取っておいたりしないといけないのかな?

messageの順番を並び替え(ASC, DESC)
lib/appwrite/database_api.dart
Future<DocumentList> getMessages() {
return databases.listDocuments(
databaseId: APPWRITE_DATABASE_ID,
collectionId: COLLECTION_MESSAGES,
queries: [ //追加してみた
Query.orderDesc(""),
Query.limit(2) //テスト用。単純にQueryが効いているか確認
]);
}
VSCodeだと、丸い矢印(再起動)をクリックしてリフレッシュしたらQueryが実行された