본문 바로가기
플러터 기술

플러터(flutter) 파이어베이스 Auth 로 휴대폰(SMS) 본인인증 무료로 간단히 하기(꼼수)

by 타이싸란 2021. 6. 17.

초기 발행 : 2021년 6월

실습 환경 : 맥북 / BigSur / Intel

 

앱 회원가입 부분 중에 특히나 휴대폰 인증이 필요한 앱이 있죠. 가령 쇼핑 앱 같은 거요.
그럴 때 휴대폰 인증 코드를 보내고 그걸 받아 넣으면 인증이 되는 거 같은 그런 형태를 많이 보셨을 거에요.

저렇게 인증요청을 누를 수 있게 만들어 둔 거죠.

저게 문자 서비스를 이용하는 거라 사용하게 되면 비용이 발생하게 돼요.
건당 10원에서 20원? 정도가 발생하죠.
한 달에 1,000명이 인증요청 SMS를 받는다고 하면 만 원 정도 발생하는데
한 달에 1,000명 가입만 해준다면야 만 원 정도는 쓸 수야 있죠.
그런데 어쨌든 유료 서비스이기 때문에 계약을 한다거나 조금 복잡할 수도 있고
귀찮은 일이 발생할 수 있기 때문에 개발과정에서는 좀 꺼려지더라고요.

그래서 어떻게 할까 생각하는 와중에 Firebase에서 제공하는 Auth 기능 중에 휴대폰 번호로 가입하는 기능이 있다는 게
생각나서 이거를 어떻게 잘 활용하면 좋을 거 같다는 생각이 들었어요.

원래는 SMS Auth를 통해 문자를 보내고 인증 코드 입력이 완료되면 Auth에 사용자로 등록이 되고, 로그인이 바로 되거든요.
그래서 앱 내에 firebase의 auth에 Current User 정보에 담기게 돼요.

근데 문제는 우리가 휴대폰 번호로 Auth를 가입할 것이 아니라 이메일로 Auth 가입을 할 거란 거죠.



그래서 아이디어가


1. 앱 내에 회원가입 다트 페이지에서  bool authOk=false; 라는 변수를 둔다.

2. SMS 인증 코드를 발송한다.

3. 인증 코드를 적는다.

4. 인증 코드가 유효하면 Firebase PhoneAuth 에 가입이 된다. 즉 휴대폰이 유효함.

5. 가입이 완료된 뒤 앱 내에 author=true 로 setState한다.

6. 4번에서 가입된 정보를 delete 한다.

7. 이메일 정보와 비밀번호를 모두 기재한다.

8. authOk변수가 true이면 회원가입 버튼이 활성화되면서 Firebase auth 이메일 가입으로 회원 가입한다.

(authOk 변수가 false인 상태에서 회원가입을 누르면 Toast로 휴대폰 인증을 완료하시라는 안내를 띄운다)



이렇게 하면 될 거 같아요.

일단 Firebase 콘솔 들어가셔서 Auth 부분에 전화인증 쓴다고 사용 설정을 해주세요.

 

그럼 코딩을 들어가 보겠습니다.

1. 먼저 필요한 변수들을 생성해줍니다.

위에 TextEditingController 랑 FocusNode는 설명이 필요 없을 거 같아요.


bool authOk=false;  - 폰인증이 정상적으로 완료됐는지 안됐는지 여부
bool requestedAuth=false; - 폰인증 요청을 보냈는지 여부. 인증 코드(OTP 6자리) 를 칠 수 있는 컨테이너의 visible 결정
String verificationId; -폰인증 시 생성되는 값
bool showLoading = false; 폰인증 보낼 때와 로그인할 때 완료될 때까지 뺑뺑이 화면 보일 수 있도록 하는 장치

 

 

2. auth 관련 변수 및 함수


(1) FirebaseAuth _auth = FirebaseAuth.instance; 로 등록해주면
앞으로 FirebaseAuth.instance라고 길게 칠 필요 없이 _auth 변수를 사용하면 될 거예요.

(2) 그리고 signInWithPhoneAuthCredenti 이라는 함수를 만들어주고 PhoneAuthCredential 을 받도록 했어요.
그래서 이 함수가 실행이되면  4. 단계 쭉 아래에서 생성된 PhoneAuthCredential 정보로 phoneAuth 가입을 시도하고,
(함수가 SignIn 이라는 단어가 들어가는데 원래 상식적으로 없는 아이디를 생성하는 건 create인데 이건 그냥 무조건 signIn이에요.
signin으로 함수를 실행해서 기존에 없으면 생성하고, 기존에 있으면 기존값을 쓰는 그런 형태인 거 같아요.)

(3) 가입이 완료되면 authOk = true로 바꿔요. 즉 가입이 됐다는 것은 폰번호 인증에 성공했다는 뜻이 되겠죠.
그리고 requestedAuth=false; 로 바꿈으로써 휴대폰 번호를 적을 수 있는 공간과 OTP 적을 수 있는 공간을 비활성 시켜서
못건들이게 바꿔줘요. 왜냐면 인증이 됐으니 더는 건드리지 못하게 하는 거죠.

(4) 가입이 성공적으로 끝나면 Firebase 콘솔에 Auth정보가 남기 때문에 회원탈퇴처럼 delete 지워주고.
앱내에 firebase 현재유저로 등록되어있으니 SignOut 을 해줘요.

(5) 폰번호 인증이 인증번호가 틀리든 뭐든 어떠한 이유로서 실패하면 에러를 내뱉게 되고 try catch에 예외처리에 걸리게 되죠.
아래 } FirebaseAuthException catch(e) 부분에 보면 에러에 걸리면 showLoading = false; 가 될 뿐 아무 일도 일어나지 않게 했어요.

 

3. 대략적으로 꾸미기

 

TextField 를 통해 대략적으로 꾸며주세요.

 

4.인증요청 누르기

인증 요청을 누르게 되면 verifyPhoneNumber 가 발동이 돼요.

아래 codeSent 부분을 보게 되면
코드를 성공적으로 보내면 requestedAuth = true; 로 바꿔주면서 인증코드를 누를 수 있도록 칸이 나오도록 했어요.

그리고 중요한 게 verificationId 가 만들어지는데 이값을 앱 내부의 verificationId 로 넣어요.
this.verificationId= verificationId; 이 부분이죠.

 

 

인증 요청을 누르면 이렇게 코드가 오게 되면 번호를 적고 확인을 눌러요.

 

확인을 눌러주게되면  4번에서 만들어준  verificationId 와 OTP 6자리를 토대로 PhoneAuthCredential 을 생성하고 그걸로
맨 위에 2.에서 만들어주었던 signInwithPhoneAuthCredenti 을 시도해요.

그래서 성공하면
authOk = false; 로 바뀌게 되어 인증 완료가 되고, 가입하기 눌렀을 때 인증 절차를 해주세요 라는 경고문은 피할 수가 있죠.

다시 requestedAuth=false; 되면서 인증번호 적는 부분이 사라져서 못건들이게 해요.
또한 auth에 폰으로 가입된 부분이 지워지게 되고, SignOut까지 돼요.

 

 

5. 나머지 부분 채운 뒤 이메일로 가입 완료하기

상단에 signUpUserCredential 이라는 함수를 하나 만들어 줬어요.
그리고 email 과 password를 받고 일반적인 이메일로 Auth 가입하기 절차를 따르게 돼요.

 

그래서 가입하기를 누르면 각종 예외처리가 해결되면
signUpUserCredential 함수를 발동시켜서 가입하게 하는 거죠.
이 부분은 firebas Auth email 가입을 해봤다면 쉽게 할 수 있어요.

 

가입하기를 누르면 이렇게 가입이 되어 있죠.

그리고 완료되면 get.back 한다거나 어떤 행위는 각자 넣으시면 되요.

 

이렇게 꼼수 적으로 Firebase PhoneAuth 를 사용해서 폰인증을 해보았어요.


단점은

한 개 번호당 1시간당 4번까지만 발송할 수 있다고 하네요.
또 인증번호 오는 문자 내용을 바꿀 수가 없어요. 바꿀 수 있으면 스팸이라든지 악용할 수가 있기 때문이죠.



그럼 경우에 따라서 잘 활용하시고,
문자 내용을 바꿔야 한다거나 하면 소액을 내고 유료서비스를 이용하시면 됩니다.

 

가입페이지에 대한 부분만 소스코드를 올립니다.

dart 소스코드 올리는 부분 깔끔하게 올릴수 있는 방법 아시는분좀 가르쳐주세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
 
class PhoneAuthTest extends StatefulWidget {
 
  @override
  _PhoneAuthTestState createState() => _PhoneAuthTestState();
}
 
class _PhoneAuthTestState extends State<PhoneAuthTest> {
 
  TextEditingController emailController = TextEditingController();
  TextEditingController passwordController = TextEditingController();
  TextEditingController verifyPasswordController = TextEditingController();
  TextEditingController phoneNumberController1 = TextEditingController();
  TextEditingController phoneNumberController2 = TextEditingController();
  TextEditingController otpController = TextEditingController();
 
 
  FocusNode passwordFocusNode=FocusNode();
  FocusNode verifyPasswordFocusNode=FocusNode();
  FocusNode phoneNumberFocusNode1=FocusNode();
  FocusNode phoneNumberFocusNode2=FocusNode();
  FocusNode otpFocusNode=FocusNode();
 
 
  bool authOk=false;
 
  bool passwordHide=true;
  bool requestedAuth=false;
  String verificationId;
  bool showLoading = false;
 
  FirebaseAuth _auth = FirebaseAuth.instance;
  void signInWithPhoneAuthCredential(PhoneAuthCredential phoneAuthCredential) async {
    setState(() {
      showLoading = true;
    });
    try {
      final authCredential = await _auth.signInWithCredential(phoneAuthCredential);
      setState(() {
        showLoading = false;
      });
      if(authCredential?.user != null){
        setState(() {
          print("인증완료 및 로그인성공");
          authOk=true;
          requestedAuth=false;
        });
        await _auth.currentUser.delete();
        print("auth정보삭제");
        _auth.signOut();
        print("phone로그인된것 로그아웃");
      }
 
    } on FirebaseAuthException catch (e) {
      setState(() {
        print("인증실패..로그인실패");
        showLoading = false;
      });
 
      await Fluttertoast.showToast(
          msg: e.message,
          toastLength: Toast.LENGTH_SHORT,
          timeInSecForIosWeb: 1,
          backgroundColor: Colors.red,
          fontSize: 16.0
      );
 
    }
  }
 
  Future<UserCredential> signUpUserCredential({String email,String password}) async {
    try {
      return await _auth.createUserWithEmailAndPassword(email: email, password: password);
    } catch (e) {
      void errorToast(String message){
        Fluttertoast.showToast(
            msg: message,
            toastLength: Toast.LENGTH_SHORT,
            timeInSecForIosWeb: 1,
            backgroundColor: Colors.red,
            fontSize: 16.0
        );
      }
      switch (e.code) {
        case "email-already-in-use":
          errorToast("이미 사용중인 이메일입니다");
 
          break;
        case "invalid-email":
          errorToast("잘못된 이메일 형식입니다");
          break;
        case "operation-not-allowed":
          errorToast("사용할 수 없는 방식입니다");
 
          break;
        case "weak-password":
          errorToast("비밀번호 보안 수준이 너무 낮습니다");
 
          break;
        default:
          errorToast("알수없는 오류가 발생했습니다");
      }
      return null;
    }
 
  }
 
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Firebase 폰인증 꼼수'),),
      body: Stack(
 
        children: [
          Container(
 
            child: Column(
              children: [
                Row(
                  children: [
                    Expanded(
                        flex: 1,
                        child: Text("이메일")),
                    Expanded(
                      flex: 3,
                      child: Container(
                        child: TextFormField(
 
                          style: TextStyle(
                            fontSize: 12,
                          ),
                          decoration: InputDecoration(
                            contentPadding: new EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
                            isDense: true,
                            hintText: "이메일 입력",
                            enabledBorder: OutlineInputBorder(
 
                              borderSide: BorderSide(color: Colors.grey),
                            ),
                            focusedBorder: OutlineInputBorder(
                              borderSide: BorderSide(color: Colors.red),
                            ),
                          ),
                          textInputAction: TextInputAction.next,
                          onEditingComplete: () => FocusScope.of(context).requestFocus(passwordFocusNode),
                          keyboardType: TextInputType.emailAddress,
                          controller: emailController,
 
                        ),
                      ),
                    ),
 
                  ],
                ),
                SizedBox(height: 10,),
                Row(
 
                  children: [
                    Expanded(
                        flex: 1,
                        child: Text("비밀번호")),
                    Expanded(
                      flex: 3,
                      child: Container(
 
                        child: TextFormField(
                          style: TextStyle(
                            fontSize: 12,
                          ),
                          decoration: InputDecoration(
                            contentPadding: new EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
                            isDense: true,
                            hintText: "비밀번호 입력",
                            enabledBorder: OutlineInputBorder(
                              borderSide: BorderSide(color: Colors.grey),
                            ),
                            focusedBorder: OutlineInputBorder(
                              borderSide: BorderSide(color: Colors.red),
                            ),
                          ),
                          textInputAction: TextInputAction.done,
                          keyboardType: TextInputType.visiblePassword,
                          onEditingComplete: () => FocusScope.of(context).requestFocus(verifyPasswordFocusNode),
                          focusNode:passwordFocusNode,
                          obscureText: passwordHide,
                          controller: passwordController,
 
                        ),
                      ),
                    ),
 
                  ],
                ),
                SizedBox(height: 5,),
                Row(
 
                  children: [
                    Expanded(
                        flex: 1,
                        child: Text("")),
                    Expanded(
                      flex: 3,
                      child: Container(
                        child: TextFormField(
                          style: TextStyle(
                            fontSize: 12,
                          ),
                          decoration: InputDecoration(
                            contentPadding: new EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
                            isDense: true,
                            hintText: "비밀번호 재입력",
                            enabledBorder: OutlineInputBorder(
                              borderSide: BorderSide(color: Colors.grey),
                            ),
                            focusedBorder: OutlineInputBorder(
                              borderSide: BorderSide(color: Colors.red),
                            ),
                          ),
                          textInputAction: TextInputAction.done,
                          keyboardType: TextInputType.visiblePassword,
                          focusNode:verifyPasswordFocusNode,
                          obscureText: passwordHide,
                          controller: verifyPasswordController,
 
                        ),
                      ),
                    ),
 
                  ],
                ),
                SizedBox(height: 10,),
                Row(
 
                  children: [
                    Expanded(
                        flex: 1,
                        child: Text("휴대폰")),
                    Expanded(
                      flex: 3,
                      child: Row(
 
                        children: [
                          Expanded(
                              child: Row(
                                children: [
                                  Expanded(
                                      flex: 1,
                                      child: numberInsert(
                                        editAble: false,
                                        hintText: "010",
 
                                      )),
                                  SizedBox(width: 5,),
                                  Expanded(
                                    flex: 1,
                                    child: numberInsert(
                                      editAble: authOk?false:true,
                                      hintText: "0000",
                                      focusNode: phoneNumberFocusNode1,
                                      controller: phoneNumberController1,
                                      textInputAction: TextInputAction.next,
                                      maxLegnth: 4,
                                      widgetFunction: (){
                                        FocusScope.of(context).requestFocus(phoneNumberFocusNode2);
                                      },
 
                                    ),
                                  ),
 
                                  SizedBox(width: 5,),
                                  Expanded(
                                    flex: 1,
                                    child: numberInsert(
                                      editAble: authOk?false:true,
                                      hintText: "0000",
                                      focusNode: phoneNumberFocusNode2,
                                      controller: phoneNumberController2,
                                      textInputAction: TextInputAction.done,
                                      maxLegnth: 4,
 
                                    ),
                                  ),
                                ],
                              )
                          ),
                          SizedBox(width: 5,),
                          authOk?ElevatedButton(
 
                              child:Text("인증완료")):
                          phoneNumberController1.text.length==4&&phoneNumberController2.text.length==4
                              ?
                          ElevatedButton(
                              onPressed: ()async{
                                setState(() {
                                  showLoading = true;
                                });
                                await _auth.verifyPhoneNumber(
                                  timeout: const Duration(seconds: 60),
                                  codeAutoRetrievalTimeout: (String verificationId) {
                                    // Auto-resolution timed out...
                                  },
                                  phoneNumber: "+8210"+phoneNumberController1.text.trim()+phoneNumberController2.text.trim(),
                                  verificationCompleted: (phoneAuthCredential) async {
                                    print("otp 문자옴");
                                  },
                                  verificationFailed: (verificationFailed) async {
                                    print(verificationFailed.code);
 
                                    print("코드발송실패");
                                    setState(() {
                                      showLoading = false;
                                    });
                                  },
                                  codeSent: (verificationId, resendingToken) async {
                                    print("코드보냄");
                                    Fluttertoast.showToast(
                                        msg: "010-${phoneNumberController1.text}-${phoneNumberController2.text} 로 인증코드를 발송하였습니다. 문자가 올때까지 잠시만 기다려 주세요.",
                                        toastLength: Toast.LENGTH_SHORT,
                                        timeInSecForIosWeb: 1,
                                        backgroundColor: Colors.green,
                                        fontSize: 12.0
                                    );
                                    setState(() {
                                      requestedAuth=true;
                                      FocusScope.of(context).requestFocus(otpFocusNode);
                                      showLoading = false;
                                      this.verificationId = verificationId;
                                    });
                                  },
                                );
 
                              },
                              child:Text("인증요청"))
                              :ElevatedButton(
 
                              child:Text("인증요청")),
                        ],
                      ),
                    ),
 
 
                  ],
                ),
                SizedBox(height: 5,),
                authOk?SizedBox():Visibility(
                  visible: requestedAuth,
                  child: Row(
 
                    children: [
                      Expanded(
                          flex: 1,
                          child: Text("")),
                      Expanded(
                        flex: 3,
                        child:Row(
 
                          children: [
                            Expanded(
                              child: numberInsert(
                                editAble: true,
                                hintText: "6자리 입력",
                                focusNode: otpFocusNode,
                                controller: otpController,
                                textInputAction: TextInputAction.done,
                                maxLegnth: 6,
 
                              ),
                            ),
                            SizedBox(width: 5,),
                            ElevatedButton(
                                onPressed: (){
                                  PhoneAuthCredential phoneAuthCredential =
                                  PhoneAuthProvider.credential(
                                      verificationId: verificationId, smsCode: otpController.text);
 
                                  signInWithPhoneAuthCredential(phoneAuthCredential);
                                },
                                child: Text("확인")),
                          ],
                        ),
                      ),
 
                    ],
                  ),
                ),
                SizedBox(height: 10,),
                ElevatedButton(
                  child: Text('가입하기'),
                  onPressed: ()async{
                    if(emailController.text.length>1&&passwordController.text.length>1&&verifyPasswordController.text.length>1){
                      if(passwordController.text==verifyPasswordController.text){
                        if(authOk){
                          setState(() {
                            showLoading=true;
                          });
 
                          await signUpUserCredential(email:emailController.text ,password:passwordController.text );
 
                          setState(() {
                            showLoading=false;
                          });
                        }else{
                          Fluttertoast.showToast(
                              msg: "휴대폰 인증을 완료해주세요.",
                              toastLength: Toast.LENGTH_SHORT,
                              timeInSecForIosWeb: 1,
                              backgroundColor: Colors.red,
                              fontSize: 16.0
                          );
 
                        }
                      }else{
                        Fluttertoast.showToast(
                            msg: "비밀번호를 확인해 주세요.",
                            toastLength: Toast.LENGTH_SHORT,
                            timeInSecForIosWeb: 1,
                            backgroundColor: Colors.red,
                            fontSize: 16.0
                        );
                      }
                    }
                    else{
 
                      Fluttertoast.showToast(
                          msg: "이메일 및 비밀번호를 입력해 주세요.",
                          toastLength: Toast.LENGTH_SHORT,
                          timeInSecForIosWeb: 1,
                          backgroundColor: Colors.red,
                          fontSize: 16.0
                      );
                    }
 
                  },
                  style: ElevatedButton.styleFrom(
                      primary: Colors.black,
                      padding: EdgeInsets.symmetric(horizontal: 50, vertical: 10),
                      minimumSize: Size(double.infinity,0),
 
 
 
                      textStyle: TextStyle(
 
                          fontWeight: FontWeight.bold)),
                ),
                SizedBox(height: 50,),
              ],
            ),
          ),
 
          Positioned.fill(
            child: Visibility(
                visible: showLoading,
                child: Container(
                    width: double.infinity,
                    height: double.infinity,
 
                    child: Center(child: Container(
                        width: MediaQuery.of(context).size.width*0.9,
                        height: 80,
                        color: Colors.white,
                        child: Center(child: Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            SizedBox(
                                width: 20,
                                height: 20,
                                child: CircularProgressIndicator()),
                            SizedBox(width: 20,),
                            Text("잠시만 기다려 주세요"),
                            SizedBox(width: 20,),
                            Opacity(
                              opacity: 0,
                              child: SizedBox(
                                  width: 20,
                                  height: 20,
                                  child: CircularProgressIndicator()),
                            ),
 
 
                          ],
                        ))))
 
                )
            ),
          )
        ],
      )
    );
  }
  Widget numberInsert(
      {
        bool editAble,
        String hintText,
        FocusNode focusNode,
        TextEditingController controller,
 
        TextInputAction textInputAction,
        Function widgetFunction,
        int maxLegnth,
 
      }){
    return TextFormField(
      enabled: editAble,
      style: TextStyle(
        fontSize: 12,
      ),
      decoration: InputDecoration(
        contentPadding: new EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
        isDense: true,
        counterText: "",
        hintText: hintText,
        enabledBorder: OutlineInputBorder(
          borderSide: BorderSide(color: Colors.grey),
        ),
        focusedBorder: OutlineInputBorder(
          borderSide: BorderSide(color: Colors.red),
        ),
        disabledBorder: OutlineInputBorder(
          borderSide: BorderSide(color: Colors.grey),
        ),
      ),
 
      textInputAction: textInputAction,
      keyboardType: TextInputType.number,
      inputFormatters: [FilteringTextInputFormatter.digitsOnly,],
      focusNode:focusNode,
      controller: controller,
      maxLength: maxLegnth,
      onChanged: (value){
 
        if(value.length>=maxLegnth){
          if(widgetFunction==null){
            print("noFunction");
          }else{
            widgetFunction();
          }
        }
        setState(() {
 
        });
      },
 
 
      onEditingComplete: (){
        if(widgetFunction==null){
          print("noFunction");
        }else{
          widgetFunction();
        }
 
      },
 
    );
  }
}
 
cs

 

 

 

 

-----------------공지-------------------

23년 버전 앱강의를 오픈했습니다. 관심있으신분은 클릭 ㅋㅋ

2023.07.29 - [코딩생초보를 위한 플러터 빠르게 한바퀴] - 1. create project

 

1. create project

1회차 세부 과정 목차 더보기 1.Flutter new project 옵션 선택 2.파일 구조와 역할 설명 3.주석 4.안드로이드 시뮬레이터 테스트 앱띄우기 5.핫리로드 6.IOS 시뮬레이터 테스트 앱띄우기 7.머티리얼앱 과

100sucoding.tistory.com

개인과외 문의 saran.flutter@gmail.com