Supabase와 Getx로 떨리는 Auth 구현
어이!!!
Firebase와 같은 백엔드 서비스로 Flight 응용 프로그램이나 사이트를 설치해야 했지만 Firebase의 모든 복잡한 설정 과정을 겪고 싶지 않으셨습니까?아니면 Firebase 사용이 지겨워서 다른 백엔드 서비스를 사용하고 싶은 건지.😂?
됐어, 영웅이 왔어.슈퍼맨 구하기!!!!
잠깐만, 슈퍼맨 아니야.😬, 수파베스입니다.
Supabase란?
Supabase는 Firebase의 대안으로 개발된 오픈 소스 백엔드 즉 서비스입니다.이것은 개발자 응용 프로그램이나 웹 응용 프로그램에 필요한 많은 기능을 제공하는 많은 다른 소스 패키지와 도구를 사용하여 구축된 것이다.그것은 관계 데이터베이스(PostgreSQL 데이터베이스), 내장된 신분 검증과 권한 수여, 저장, 실시간 구독 등이 있습니다!
A relational database is one that stores data which have some relationships between them
Supabase는 현재 공개 테스트 중이며, 출시되거나 생산을 준비할 때 더욱 많은 특성과 기능을 가지게 될 것이다.Supabase에는 가장 좋은 문서 중 하나가 있습니다.그들의 사이트를 살펴보다.
https://supabase.io/
우리 뭐하지?
Flatter 응용 프로그램을 만들고 Supabase를 사용하여 인증을 설정합니다.
Get은 이 응용 프로그램에서 사용됩니다.
Get is a package used for state management , route management, and dependency injection in flutter. It's quite easy to understand and get started with it.
State management - Apps and websites have something called state. Whenever a user interacts with the app , the state of the app changes(in simple words , the app reacts to the user's action) . This state needs to be managed to define how and when it should change. This is done using the state management technique. Flutter comes with a built-in state management technique - setstate.
Route management - Sometimes we may need to show different screens to the user , this is done using route management.
Dependency Injection - Some objects in the app depend on another for its functioning , this is called dependency. To give the object what it needs is dependency injection(It's like passing a service to a client). With this , the object can be accessed anywhere in the widget tree easily.
https://pub.dev/packages/get
개시하다
첫걸음떨림 프로그램을 만듭니다.
두 번째 단계.Supabase로 이동하고 프로젝트 시작 을 클릭합니다.
세 번째.Supabase에 새로 오신 분이라면 로그인 페이지로 안내해 드리겠습니다.로그인한 경우 6단계로 건너뜁니다.
네 번째 단계.Github을 계속 사용하려면 을 클릭합니다.
다섯 번째.자격 증명을 입력하고 로그인 을 클릭합니다.
여섯 번째.새 항목을 클릭하십시오.
일곱 번째.프로젝트에 이름을 붙이고 강력한 비밀번호를 입력한 다음 위치에서 가장 가까운 서버 영역을 선택하십시오.
여덟 번째.앉아서 커피 한잔 마시고 수파베이스에서 프로젝트를 만들어 드릴게요.
Meanwhile you can check out their documentation and API references on their website.
아홉 번째.준비가 되면 설정으로 이동한 다음 API 섹션으로 이동합니다.
열 번째.프로젝트 URL과 프로젝트 API 키를 적어 두십시오. 프로그램에 필요합니다.이것이 바로 우리가 응용 프로그램에 백엔드 서비스를 설정해야 하는 모든 내용이다.컴파일과 관련된 다른 프로그램은 없습니다.gradle 파일 등, 예를 들어Firebase.
Now go the Authentication section and then into settings and disable email confirmations or else we'll have to verify each email before it we sign In.
11번.테이블 편집기 섹션으로 이동하여 새 테이블 작성을 클릭합니다.
12번.이름을 Users로 지정하고 나머지는 기본값으로 설정한 다음 Save를 클릭합니다.
우리는 이 표를 사용하여 등록된 사용자의 사용자 데이터를 저장할 것이다.
13번.현재 id열 옆에 있는 "+"아이콘을 누르면 새 열을 만듭니다.
We'll name it Name and set the type to text and unselect Allow nullable because we don't want the name of the user to be null by accident.
14번.Email 및 user Id를 저장하기 위해 두 열의 Email 및 Id를 더 만들 것입니다.
이렇게 해서 지금 우리의 데이터베이스가 준비되었다!!.
애플리케이션 구축
app 폴더를 열고pubspec에 들어갑니다.yaml 및 다음 패키지를 가져옵니다.
https://pub.dev/packages/supabase
https://pub.dev/packages/get_storage
https://pub.dev/packages/get
https://pub.dev/packages/form_field_validator
get_storage - Now let's assume that a user logs in to the app and uses it for some time and then exits the app. The next time he opens the app , it shouldn't take him to the login page again, right? So , we need to store a session string for the user so that the app takes him to the home page. This is done using this package.
이제 메인으로 가자.채널을 열고 다음 코드를 붙여넣습니다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Wrapper(),
);
}
}
class Wrapper extends StatefulWidget {
@override
WrapperState createState() => WrapperState();
}
class WrapperState extends State<Wrapper> {
@override
void initState() {
// TODO: implement initState
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
The Wrapper class is used to listen to auth changes(whether the user is logged in or not) in the app and take him to the appropriate page.
두 개의 전역 변수를 만들고 이전에 복사한 값을 그들에게 분배합니다.
final String _supaBaseUrl = 'Project URL';
final String _supaBaseKey = 'Project API key';
주로 이런 것들을 수입한다.던지다import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:supabase/supabase.dart';
현재 void main에 의존 항목을 만듭니다void main() {
Get.put<SupabaseClient>(SupabaseClient(_supaBaseUrl, _supaBaseKey));
Get.put<GetStorage>(GetStorage());
runApp(MyApp());
}
We create a SupabaseClient dependency to access all the functions of supabase.
Get.put() takes in a dependency to inject as an argument. We specify the type of dependency using less than and greater than operators.SupabaseClient takes in 2 arguments - project URL and project API key.
We will also create a dependency for storing the session string and the type will be GetStorage.
lib 폴더에 새 파일을 만들고authService라고 이름을 붙입니다.던지다.
AuthService라는 별도의 클래스에서 어플리케이션의 인증 기능을 구현합니다.
다음 코드를 붙여넣습니다.
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:supabase/supabase.dart';
class AuthService {
final _authClient=Get.find<SupabaseClient>();
//register user and create a custom user data in the database
//log in user
//get currently logged in user data
//logOut
//RecoverSession
}
Get.find() finds the injected dependency instance in the whole widget tree and returns it. In our case, it is located in main.dart. We store it in a variable and use it later.
사용자 등록 및 사용자 데이터 만들기
//register user and create a custom user data in the database
Future<GotrueSessionResponse> signUpUser(String name, String email, String password) async {
final user =
await _authClient.auth.signUp(email, password);
final response =
await _authClient.from('Users').insert([
{"Id": user.user.id, "Name": name, "Email": email}
]).execute();
if (response.error == null) {
return user;
}
}
We create a function signUpUser() which returns a Future of type GotrueSessionResponse.
Future - Sometimes we need to retrieve data from the database or somewhere else where the data may not be readily available or may take some time to load depending upon your internet connection. Such data are called Futures.To use them in our code , we need to mark that part of code async(meaning asynchronous) and use the keyword await to await the data to arrive. When we use await in our code , whatever code comes after that await code line is executed only after the data arrives(or after the awaited code is completely executed).To register the user , we use the _authClient variable and tap into the auth property and then use the signUp() function.It takes 2 arguments - email and password.Since it return a Future of type GotrueSessionResponse , we use await.
To create user data in our database, we use the _authClient variable and use the from() function which takes in the database name as an argument, and use the insert function which takes a list of Maps as an argument. Finally, we execute it.
Syntax : _authClient.from("Database name").insert([
{"Column_1_name":value, "Column_2_name":value , and so on}, {"Column_1_name":value, "Column_2_name":value , and so on},
and so on
]).execute();
로그인 사용자
//log in user
Future<GotrueSessionResponse> signIn(String email, String password) async {
final user =
await _authClient.auth.signIn(email: email, password: password);
if (user.error == null) {
return user;
}
}
To log in the user , we use the _authClient variable and tap into the auth property and then use the signIn() function.It takes 2 named arguments - email and password.Since it return a Future of type GotrueSessionResponse , we use await.
현재 사용자 가져오기
//get currently logged in user data
User getCurrentUser() {
return _authClient.auth.user();
}
The user() function returns user data of type User if there is a currently logged in user.
사용자 로그아웃
//logOut
Future<GotrueResponse> logOut() async {
await _authClient.auth.signOut();
}
The signOut() function simply signs out the current user(if there is a logged-in user) and returns a Future of type GotrueResponse. It takes no arguments.
사용자 세션 재개
//RecoverSession
Future<GotrueSessionResponse> recoverSession(String session) async {
return await _authClient.auth.recoverSession(session);
}
This function is to recover user session if a user has logged in , used the app for some time, and exited. It takes a String as an argument and returns a Future of type GotrueSessionResponse.
사용자 인터페이스
이제는 우리 앱에 화장품을 첨가해서 더 예뻐 보일 때가 됐다.
lib 폴더로 이동하여 loginPage라는 파일을 만듭니다.채널을 열고 다음 코드를 붙여넣습니다.
import 'package:supabase_auth/Screens/Auth/registerPage.dart';
import 'package:supabase_auth/Screens/Home/home.dart';
import 'package:supabase_auth/Services/authService.dart';
import 'package:flutter/material.dart';
import 'package:form_field_validator/form_field_validator.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
class LoginPage extends StatefulWidget {
String email = '';
LoginPage({this.email});
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
AuthService _service = AuthService();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool logging = false, obscure = true;
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
_emailController.text = widget.email;
return Scaffold(
body: SingleChildScrollView(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Padding(
padding: EdgeInsets.only(top: 100.0),
child: Center(
child: Text(
'LOGIN',
style: TextStyle(
color: Colors.black,
fontSize: 50,
),
),
),
),
Container(
margin: EdgeInsets.only(
top: size.height / 6,
left: 40.0,
right: 40.0,
),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20.0),
),
child: Padding(
padding: EdgeInsets.only(
top: 20.0,
left: 20.0,
right: 20.0,
),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(
hintText: "Email",
hintStyle: TextStyle(color: Colors.white),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
),
validator: MultiValidator([
RequiredValidator(errorText: "Required"),
EmailValidator(
errorText:
"Please enter a valid email address"),
]),
),
SizedBox(
height: 20.0,
),
TextFormField(
obscureText: true,
controller: _passwordController,
decoration: InputDecoration(
hintText: "Password",
hintStyle: TextStyle(color: Colors.white),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
),
validator: MultiValidator([
RequiredValidator(errorText: "Required"),
MinLengthValidator(6,
errorText:
"Password must contain atleast 6 characters"),
MaxLengthValidator(20,
errorText:
"Password must not be more than 20 characters"),
]),
),
SizedBox(
height: 20.0,
),
logging == false
? ElevatedButton(
onPressed: () async {
if (_formKey.currentState.validate()) {
setState(() {
logging = true;
});
login();
}
},
child: Padding(
padding:
EdgeInsets.symmetric(horizontal: 50.0),
child: Text(
'Login',
style: TextStyle(color: Colors.black),
),
),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
),
backgroundColor:
MaterialStateProperty.all(Colors.white),
),
)
: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Colors.black),
),
SizedBox(
height: 20.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Don't have an account? ",
style: TextStyle(color: Colors.white),
),
InkWell(
onTap: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => RegisterPage(),
),
);
},
child: Text(
"Register",
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold),
),
),
],
),
SizedBox(height: 20.0),
],
),
),
),
),
],
),
),
),
);
}
Future login() async {}
}
SnackBar snackBar({String content, String type}) => SnackBar(
content: Text(
content,
style: TextStyle(
color: Colors.white,
fontSize: 20.0,
),
),
backgroundColor: type == "Error" ? Colors.red : Colors.green,
);
}
lib 폴더로 이동하여 registerPage라는 파일을 만듭니다.채널을 열고 다음 코드를 붙여넣습니다.import 'package:supabase_auth/Screens/Auth/loginPage.dart';
import 'package:supabase_auth/Services/authService.dart';
import 'package:flutter/material.dart';
import 'package:form_field_validator/form_field_validator.dart';
class RegisterPage extends StatefulWidget {
@override
_RegisterPageState createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
AuthService _service = AuthService();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _nameController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool registering = false;
bool obscure = true;
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Scaffold(
body: SingleChildScrollView(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Padding(
padding: EdgeInsets.only(top: 100.0),
child: Center(
child: Text(
'REGISTER',
style: TextStyle(
color: Colors.black,
fontSize: 50,
),
),
),
),
Container(
margin: EdgeInsets.only(
top: size.height / 6,
left: 40.0,
right: 40.0,
),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20.0),
),
child: Padding(
padding: EdgeInsets.only(
top: 20.0,
left: 20.0,
right: 20.0,
),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: InputDecoration(
hintText: "Name",
hintStyle: TextStyle(color: Colors.white),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
),
validator: MultiValidator([
RequiredValidator(errorText: "Required"),
]),
),
SizedBox(
height: 20.0,
),
TextFormField(
controller: _emailController,
decoration: InputDecoration(
hintText: "Email",
hintStyle: TextStyle(color: Colors.white),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
),
validator: MultiValidator([
RequiredValidator(errorText: "Required"),
EmailValidator(
errorText:
"Please enter a valid email address"),
]),
),
SizedBox(
height: 20.0,
),
TextFormField(
obscureText: true,
controller: _passwordController,
decoration: InputDecoration(
hintText: "Password",
hintStyle: TextStyle(color: Colors.white),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
),
validator: MultiValidator([
RequiredValidator(errorText: "Required"),
MinLengthValidator(6,
errorText:
"Password must contain atleast 6 characters"),
MaxLengthValidator(20,
errorText:
"Password must not be more than 20 characters"),
]),
),
SizedBox(
height: 20.0,
),
registering == false
? ElevatedButton(
onPressed: () async {
if (_formKey.currentState.validate()) {
setState(() {
registering = true;
});
register();
}
},
child: Padding(
padding:
EdgeInsets.symmetric(horizontal: 50.0),
child: Text(
'Register',
style: TextStyle(color: Colors.black),
),
),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
),
backgroundColor:
MaterialStateProperty.all(Colors.white),
),
)
: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Colors.black),
),
SizedBox(
height: 20.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Already have an account? ",
style: TextStyle(color: Colors.white),
),
InkWell(
onTap: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => LoginPage(email: _emailController.text,),
),
);
},
child: Text(
"Login",
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold),
),
),
],
),
SizedBox(height: 20.0),
],
),
),
),
),
],
),
),
),
);
}
Future register() async { }
SnackBar snackBar({String content, String type}) => SnackBar(
content: Text(
content,
style: TextStyle(
color: Colors.white,
fontSize: 20.0,
),
),
backgroundColor: type == "Error" ? Colors.red : Colors.green,
);
}
다음은 메인으로.dart 및 inside Wrapper 위젯, 다음 내용 붙여넣기void sessionCheck() async {
await GetStorage.init();
final box = Get.find<GetStorage>();
AuthService _authService = AuthService();
final session = box.read('user');
if (session == null) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => LoginPage(),
),
);
} else {
final response = await _authService.recoverSession(session);
await box.write('user', response.data.persistSessionString);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => HomePage(),
),
);
}
}
We await GetStorage() to start/initialise the storage drive. Next, we find a GetStorage dependency instance and assign it to a variable called box.
We then use this box variable and call a function read() which reads the storage drive/container and checks if there is a value associated with the key we pass as an argument to it. The value will be a session string and we store it in a variable named session. Why so? Because it makes sense😂.
If there is no value present , then we take the user to the LoginPage.
Else if there is some value present , we recover that session by calling the recoverSession() function we had defined in the AuthService class. We call that function using an instance of AuthService class.
We store the returned value in a variable called sessionResponse.Now we need to save this session again in the container so that we have access to it the next time the user opens the app.
We do it by awaiting box.write() which takes 2 arguments :
1.key = The name of the key where the value is stored. You can give it any name.
2.value = The session string to be stored in the container and it will be associated with the key we specify.
This function returns a Future of type void , so we await it. Once it is done , we go to the homePage.
현재 우리는 이미 이 함수를 정의했으니, 우리는 그것을 호출해야 한다.main에 다음 코드를 붙여넣으십시오.성도 내 포장 소부품
@override
void initState() {
// TODO: implement initState
super.initState();
sessionCheck();
}
initState() is a function that is automatically called when the widget in which it is(In this case it is Wrapper widget), is loaded onto the stack. Or in simple words, it is called when the Wrapper widget loads/fires in the app.
이제 로그인 페이지로 돌아갑니다.성도 및 그 안에 들어가는 로그인 기능.
Suppose a user registers for the first time , we show him the LoginPage right?
So , after login , his session string has to be saved so that it is available to the app the next time he opens it. We add two lines to the login function :
Future login() async {
final box = Get.find<GetStorage>();
final result =
await _service.signIn(_emailController.text, _passwordController.text);
if (result.data != null) {
await box.write('user', result.data.persistSessionString);
ScaffoldMessenger.of(context).showSnackBar(
snackBar(content: "Login successful", type: "Success"),
);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => HomePage(),
),
);
} else if (result.error?.message != null) {
ScaffoldMessenger.of(context).showSnackBar(
snackBar(content: result.error.message, type: "Error"),
);
}
}
페이지를 등록하러 가다.채널 및 등록 기능 Future register() async {
final result = await _service.signUpUser(
_nameController.text, _emailController.text, _passwordController.text);
if (result.data != null) {
setState(() {
registering = false;
});
ScaffoldMessenger.of(context).showSnackBar(
snackBar(content: "Registration Successful", type: "Success"),
);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => LoginPage(
email: _emailController.text,
),
),
);
} else if (result.error.message != null) {
setState(() {
registering = false;
});
ScaffoldMessenger.of(context).showSnackBar(
snackBar(content: result.error.message, type: "Error"),
);
}
}
홈 페이지
홈 페이지라는 파일을 만듭니다.lib 폴더에 채널을 삽입합니다.간단한 홈 페이지를 만들 것입니다. 단추 하나만 있으면 로그아웃 기능을 할 수 있습니다.
import 'package:supabase_auth/Services/authService.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
AuthService _authservice = AuthService();
bool loading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text("Home Page"),
centerTitle: true,
),
body: Center(
child: loading == false
? ElevatedButton(
onPressed: () async {
setState(() {
loading = true;
});
await logout();
},
child: Text("Log Out"),
)
: CircularProgressIndicator(),
),
);
}
Future logout() async { }
}
취소
Future logout() async {
await _authservice.logOut();
setState(() {
loading = false;
});
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => LoginPage(),
),
);
}
Just like earlier , we will use the AuthService class instance to call the logOut() function.
Since it returns a Future , we await it and once it is done , we take the user to the LoginPage.
이제 authService로 이동합니다.채널을 줄이고 로그아웃 기능으로 이동합니다.
//logOut user
Future<GotrueResponse> logOut() async {
Get.find<GetStorage>().remove('user');
await _authClient.auth.signOut();
}
Here we are removing the session string associated with the user because it is no longer needed once the user logs out. The remove() function removes the data from the container by key. It takes the key as an argument and returns a Future of type void.
어플리케이션 실행
응, 그래, 바로 이거야.계속해서 휴대전화나 시뮬레이터에서 프로그램을 실행한다.
로그인하면 데이터베이스를 검사하고 로그인 상세 정보가 저희가 만든 데이터베이스/표에 나타날 수 있습니다.
다음 단계
github에서 온전한 원본 코드를 보십시오.
Adityasubramanyabhat / supabase_auth
이것은 supabase를 사용하여 인증 기능을 보여 주는 응용 프로그램이다
supabase_auth
새로운 떨림 프로젝트
개시하다
이 항목은 떨림 응용의 출발점이다.
첫 번째 Flitter 프로젝트인 경우 다음을 시작할 수 있는 리소스를 제공합니다.
online documentation, 자습서 제공,
예제, 모바일 개발 설명서 및 전체 API 참조.
View on GitHub
감사합니다.🙏
Reference
이 문제에 관하여(Supabase와 Getx로 떨리는 Auth 구현), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/adityasubrahmanyabhat/implementing-auth-in-flutter-using-supabase-and-getx-1m14텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)