Red Glitter Pointer

 

 

flutter_oss_licenses | Dart package

A tool to generate detail and better OSS license list using pubspec.yaml/lock files.

pub.dev

 

 

1. flutter_oss_licenses 설치

 

👇 pubspec.yaml 에 추가

dev_dependencies:
  flutter_oss_licenses: ^3.0.2

 

공식홈페이지에 나와있는대로 3.0.2버전을 설치하려고 했으나 아래와 같은 에러 발생, 터미널에 나와있는대로 downgrade했다. 2.0.1로 진행함! 

 

 

👇 터미널창에 입력

flutter pub get
flutter pub run flutter_oss_licenses:generate.dart

 

🪸 두번째 명령어까지 입력하면 프로젝트 최상위에 oss_licenses.dart 파일 자동생성된다.

🪸 해당 파일 안에는 현재 프로젝트에 사용하고 있는 모든 패키지의 licenses 정보가 json 형태로 저장됨!

 

⚠️ 자동으로 생성된 oss_licenses.dart에는 모든 패키지가 다 저장되기 때문에, 명시할 패키지만 남겨두고 삭제하면 된다 

 

 

생성된 oss_licenses.dart 파일의 구조는 아래와 같다.

 

 

 

2. 페이지 생성

oss_licenses.dart에 있는 package들을 보여줄 page를 생성한다

 

👇 oss_licenses_page.dart

import 'package:flutter/material.dart';
import 'package:test/oss_licenses.dart';
import 'package:test/misc_oss_license_single.dart';

class OssLicensesPage extends StatelessWidget {
  const OssLicensesPage({super.key});

  static Future<List<String>> loadLicenses() async {
    final ossKeys = List<String>.from(ossLicenses);
    return ossKeys..sort();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('오픈소스 라이선스')),
      body: SingleChildScrollView(
        child: Column(
          children: [
            for (var i = 0; i < ossLicenses.length; i++)
              ListTile(
                title: Text(ossLicenses[i].name),
                subtitle: Text(ossLicenses[i].description),
                trailing: Icon(Icons.chevron_right),
                onTap: () {
                  Navigator.of(context).push(
                    MaterialPageRoute(
                      builder: (context) => MiscOssLicenseSingle(
                        name: ossLicenses[i].name,
                        version: ossLicenses[i].version,
                        description: ossLicenses[i].description,
                        licenseText: ossLicenses[i].license ?? '',
                        homepage: ossLicenses[i].homepage ?? '',
                      ),
                    ),
                  );
                },
              )
          ],
        ),
      ),
    );
  }
}

 

 

👇 misc_oss_license_single.dart

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

class MiscOssLicenseSingle extends StatelessWidget {
  final String name;
  final String version;
  final String description;
  final String licenseText;
  final String homepage;

  MiscOssLicenseSingle({
    super.key,
    required this.name,
    required this.version,
    required this.description,
    required this.licenseText,
    required this.homepage,
  });

  String _bodyText() {
    return licenseText.split('\n').map((line) {
      if (line.startsWith('//')) line = line.substring(2);
      line = line.trim();
      return line;
    }).join('\n');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
        child: Column(
          children: [
            ListTile(
              title: Text(name),
              subtitle: Text('version : $version'),
            ),
            Padding(
                padding:
                    const EdgeInsets.all(12),
                child: Text(description)),
            const Divider(),
            Padding(
              padding:
                  const EdgeInsets.all(12),
              child: Text(_bodyText()),
            ),
            const Divider(),
            ListTile(
              title: Text('Homepage'),
              subtitle: Text(homepage),
              onTap: () async {
                final Uri homepageUri = Uri.parse(homepage);
                if (await canLaunchUrl(homepageUri)) {
                  await launchUrl(homepageUri);
                } else {
                  throw '해당 페이지로 이동할 수 없습니다. $homepage';
                }
              },
            ),
          ],
        ),
      ),
    );
  }
}

 

 

3. 결과물

목록
누르면 위 상세 페이지로 이동!

 

 

현재 진행중인 프로젝트에서는 text나 color, header 등 style을 별도로 지정해서 사용했기 때문에 그대로 올릴 수 없어서 위에 올린 코드에는 복붙만해도 정상작동 되게끔 수정해서 올렸다. 그래서 업로드한 스크린샷과 디자인은 조금 다를 수 있음! 

  • Future와의 상호작용의 가장 최신 스냅샷을 기반으로 스스로 빌드되는 위젯
  • UI 에서 비동기 연산을 처리하는 데 주로 사용됨

 

 

개념

  • 비동기 연산(Asynchronous Operation) : 잠재적으로 값이나 에러가 될 수 있는 Future 와 함께 동작하도록 설계 됐다. 네트워크 요청이나 DB 쿼리와 같은 비동기 작업에 사용 됨.
  • 상태 관리 : Future 의 상태는 not started , in progress , completed with data, completed with error 가 있는데 이에 따라 UI를 재구성하여 비동기 작업의 상태관리를 처리한다.

 

 

주요 속성

  1. Future: 이 빌더가 연결된 Future 이다. 데이터의 소스이고 빌더는 UI를 빌드하기 위해 이 데이터를 갖다 쓴다
  2. builder : 위젯 트리를 빌드하는 함수이다. Future 의 상태에 따라 렌더링할 UI를 정의한다. BuildContext  Future 결과의 비동기 스냅샷 AsyncSnapshot 에 액세스할 수 있다.

 

 

주의할 점

  1. 든 상태 처리하기 : Future 의 모든 상태 loading , success , error 에 대한 처리를 해주자
  2. Future 재생성(Recreation) 방지 : 모든 빌드에서 퓨처가 다시 생성되지 않도록 하려면, build 메서드 외부에서 Future 를 정의하거나 Future Provider 를 사용해야 한다.
  3. 류 처리 : Future 가 오류와 함께 완료되는 경우에 대비해 꼼꼼한 오류 처리 구현

 

 

언제 사용하는가?!

  • 데이터 가져오기 : 비동기로 데이터를 가져올 때
  • 동적 콘텐츠 : 위젯의 콘텐츠가 비동기 데이터의 결과에 따라 달라지는 경우 유용하다

 

 

사용 예제 1️⃣

// 👉 FutureBuilder 위젯 생성, Future가 반환하는 데이터의 타입 == TypeOfData
// FutureBuilder는 비동기적으로 생성되는 데이터와 함께 UI를 구축하는 데 사용된다
FutureBuilder<TypeOfData>(
	// 👉 FutureBuilder에 사용할 Future 지정한다.
	// myFutureMethod()는 비동기적으로 데이터를 가져오거나 계산하는 메소드
	future: myFutureMethod(),
	// 👉 Future의 현재 상태에 따라 UI를 구축하는 함수
	// context와 snapshot 두 개의 매개변수를 받음.
	// context : 위젯의 빌드 컨텍스트
	// snapshot : Future의 현재 상태와 반환된 데이터 포함
	builder: (context, snapshot) {
		// 데이터 로드 중인 경우
		if(snapshot.connectionState == ConectionState.waiting) {
			return CircularProgressIndicator();
		} else if(snapshot.hasError) {
			return Text('Error: ${snapshot.error}');
		} else {
			return MyWidget(data: snapshot.data);
		}
	},
)

 

📢 비동기적으로 데이터를 로드하고, 로드 중에는 로딩 인디케이터를 표시하며, 오류 발생 시 오류 메시지 보여주고, 데이터가 성공적으로 로드 되면 해당 데이터를 사용하여 위젯을 생성하는 것.

 

 

 

사용 예제 2️⃣

// 👉 HomeScreen 위젯을 관리하는 클래스 정의
// _HomeScreenState 클래스는 State 클래스를 상속받아 HomeScreen 위젯의 상태를 관리한다.
class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        centerTitle: true, // 타이틀 중앙 배치
        title: const Text( // 앱 바의 제목 설정
            'CodingFactory Youtube Example'
        ),
        backgroundColor: Colors.black,
      ),
	  // 👉 FutureBuilder 위젯을 사용하여 비동기적으로 List<VideoModel> 타입의 데이터를 처리한다
      body: FutureBuilder<List<VideoModel>>(
		// 👉 FutureBuilder에서 사용할 Future를 지정한다.
		// 비동기적으로 비디오 목록을 가져올 메소드
        future: YoutubeRepository.getVideos(),
		// 👉 현재 상태에 따라 UI를 구축하는 builder 함수
        builder: (context, snapshot) {
          // 👉 completed with error 처리
          if (snapshot.hasError) {
            return Center(
                child: Text(
                    snapshot.error.toString()
                )
            );
          }

          // 👉 completed without data 처리
		  // 아직 데이터를 받지 못한 경우, 로딩중
          if (!snapshot.hasData) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }

          // 👉 completed with data 처리
		  // 로딩 완료 시, 새로고침 기능을 포함하는 RefreshIndicator 반환
          return RefreshIndicator(
			// 새로고침 동작 정의 setState 호출하여 위젯 재빌드
            onRefresh: () async {
              setState(() {});
            },
			// 스크롤 가능한 목록 생성
            child: ListView(
              physics: const BouncingScrollPhysics(), // 스크롤 효과 설정
			  // 👉 snapshot.data를 사용하여 CustomYoutubePlayer 위젯의 리스트를 생성
              children: snapshot.data!
                  .map((e) => CustomYoutubePlayer(videoModel: e))
                  .toList(),
            ),
          );
        },
      ),
    );
  }
}

 

📢 Youtube 동영상 목록을 비동기적으로 로드하고, 로딩 상태에 따라 적절한 UI(오류메시지, 로딩 인디케이터, 동영상 목록)을 표시하는 Flutter 앱의 홈 화면을 구성한다.

 

 

 

 

🌐 공식 문서

FutureBuilder class - widgets library - Dart API

 

☠️☠️ 스낵바가 설정되어있는 버튼을 여러 차례 누르면, 그 횟수만큼 계속해서 스낵바가 뜬다 ☠️☠️

 

 

해결 방법 !! 👇

if (Get.isSnackbarOpen) {

} else {
	Get.snackbar("저장 실패", "빈칸 없이 채워주세요",
		colorText: Mycolor().snackbarText,
		snackPosition: SnackPosition.TOP,
		backgroundColor: Mycolor().snackbarBg,
		icon: const Icon(Icons.warning_amber));
}

 

위 코드와 같이 if 문을 한 번 걸어서, 현재 snackbar가 떠있는 상황인지 확인 후 해당 기능을 수행하도록 변경해주면 됨!

 

 

GetX를 이용한 상태관리 방식

1. 단순 상태 관리

  • 기존의 데이터와 변경되는 데이터가 같은지 확인하지 않는다.
import 'package:get/get.dart';

// 👉 GetxController를 상속받아 GetX 패턴을 사용한 상태 관리를 위한 컨트롤러를 정의한디
class SimpleController extends GetxController {
	int counter = 0;

	void increase() {
		counter++;
		// 👉 update 메소드 호출하여 UI 갱신.
		// GetX 패턴에서 상태 변경을 알리는 방법
		update();
	}
}

 

// 👉 StatelessWidget을 상속받아 상태가 변하지 않는 위젯 정의
class MyHomePage extends StatelessWidget {
	// 👉 생성자에서 선택적 key 매개변수를 받고, 상위 클래스의 생성자에게 전달
	MyHomePage({Key? key}) : super(key: key);

	// 👉 build 메소드 오버라이드하여 UI 구성
	@override
	Widget build(BuildContext context) {
		// 👉 Getx 라이브러리의 put 메소드를 사용하여 SimpleController 인스턴스를 의존성 주입 컨테이너에 등록한다.
		Get.put(SimpleController());
		return Scaffold(
			appBar: AppBar(
				title: const Text('단순 상태관리'),
			),
			body: Center(
				// 👉 GetBuilder를 사용하여 SimpleController의 상태 변화를 감지한다.
				child: GetBuilder<SimpleController>(
					// 👉 builder 콜백에서 SimpleController 인스턴스에 접근한다.
					builder: (controller) {
						return ElevatedButton(
							child: Text('현재 숫자: ${controller.counter}',
							),
							onPressed: () {
								controller.increase();
							}
						);
					}
				)
			)
		)
	}
}

 

controller 사용하기 위해 Get.put으로 controller를 등록해준다. GetBuilder() 아래의 모든 위젯은 controller에서 변경되는 데이터를 실시간으로 반영할 수 있는 상태가 된다. controller.counter은 controller의 변수를 실시간으로 반영하게 되고 controller.increase()는 의 counter 데이터를 실시간으로 증가시키기게 된다.
만약 GetBuilder 를 사용하지 않을 경우 Get.find<Controller 종류>().[변수 혹은 함수]로 컨트롤러의 데이터를 실시간 변경 혹은 반영할 수 있다.

 

 

 

 

2. 반응형 상태 관리

  • 데이터 변화가 있을 때만 재렌더링을 하게 됨
  • workers라는 추가 기능도 있다
import 'package:get/get.dart';

class ReactiveController extends GetxController {
	// 👉 변수의 타입을 RxInt, RxString 등 Rx{타입}의 방식으로 선언하고 변수의 값은 .obs 붙임
	// update의 경우 update() 함수를 부르지 않아도 됨!
	RxInt counter = 0.obs;

	void increase() {
		counter++;
	}
}

 

class MyHomePage extends StatelessWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    Get.put(SimpleController()); // 단순 상태 관리 controller 등록
    Get.put(ReactiveController()); // 반응형 상태 관리 controller 등록
    return Scaffold(
      appBar: AppBar(
        title: const Text("단순 / 반응형 상태관리"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            GetBuilder<SimpleController>( // 단순 상태 관리
              builder: (controller) {
                return ElevatedButton(
                  child: Text(
                    '[단순]현재 숫자: ${controller.counter}',
                  ),
                  onPressed: () {
                    controller.increase();
                    // Get.find<SimpleController>().increase();
                  },
                );
              },
            ),
            GetX<ReactiveController>( // 반응형 상태관리 - 1
              builder: (controller) {
                return ElevatedButton(
                  child: Text(
                    '반응형 1 / 현재 숫자: ${controller.counter.value}', // .value 로 접근
                  ),
                  onPressed: () {
                    controller.increase();
                    // Get.find<ReactiveController>().increase();
                  },
                );
              },
            ),
            Obx( // 반응형 상태관리 - 2
                  () {
                    return ElevatedButton(
                      child: Text(
                        '반응형 2 / 현재 숫자: ${Get.find<ReactiveController>().counter.value}', // .value 로 접근
                      ),
                      onPressed: () {
                        Get.find<ReactiveController>().increase();
                      },
                    );
              },
            ),
          ],
        ),
      ),
    );
  }
}

 

controller를 사용하기 위해 Get.put() 으로 controller 등록해준다.

 

 

 

 

 

 💡 반응형 상태관리에서 데이터를 실시간으로 반영하는 방식 2가지

 

 

GetX()

  • GetX() 아래의 모든 위젯은 controller에서 변경되는 데이터를 실시간으로 반영할 수 있는 상태가 된다.
  • controller.counter.value (단순 상태 관리와는 다르게, .value를 추가해 주어야 함.)
  • controller는 변수를 실시간으로 반영하게 되고, controller.increase()는 controller의 counter 데이터를 실시간으로 증가시킴.

 

⚠️ 만약 GetX를 사용하지 않을 경우, Get.find<Controller 종류>().[변수 혹은 함수] 로 컨드롤러의 데이터를 실시간 변경 혹은 반영할 수 있다. 

 

 

 

 

Obx()

  • Obx() 아래의 모든 위젯은 GetX()와 마찬가지로 controller에서 변경되는 데이터를 실시간으로 반영할 수 있는 상태가 된다.
  • 사용 방식은 거의 동일하지만 GetX()와 달리 controller 이름을 지정할 수가 없어서 Get.find() 방식으로 접근해야 함!
 

 

 

 

 

 

 반응형 상태 관리의 추가 기능 - worker

Worker은 controller 안에서 onInit() 함수를 override 하고 그 안에 추가해서 사용하게 되는데 아래의 4가지 종류가 있다.

  • Ever: 매번 변경 될 때 실행
  • Once: 처음 변경 되었을 때만 실행
  • Interval: 계속 변경이 있는 동안 특정 지정 시간 인터벌이 지나면 실행
  • Debounce: 인터벌이 끝나고 나서 특정 시간 이후에 한 번만 실행
import 'package:get/get.dart';

class ReactiveController extends GetxController {
	static ReactiveController get to => Get.find();
	RxInt counter = 0.obs;

	@override
	void onInit() {
		once(counter, (_) {
			print('once: $_이 처음으로 변경되었습니다.');
		});

		ever(counter, (_) {
			print('ever: $_이 변경되었습니다.');
		});

		debounce(counter, (_) {
			print('debounce: $_가 마지막으로 변경된 이후, 1초간 변경이 없습니다.');
		}, time: Duration(seconds: 1), );
	
		interval(counter, (_) {
			print('interval $_가 변경되는 중입니다. (1초마다 호출)');
		}, time: Duration(seconds: 1), );

		super.onInit();
	}

	void increase() {
		counter++;
	}
}

 

TextField에서 사용자에게 받는 내용을 숫자로만 제한해야할 때가 있다.

keyboardType 옵션을 사용해서 숫자 키패드만 나오게 할 수가 있는데, 이럴 경우 문자열 붙여넣기가 가능하여 문자열이 들어갈 수 있다.

 

 

 

 

💡해결방법

숫자를 완전히 허용하지 않게 하려면, inputFormatters 옵션에서 필터링을 주면 됨!

 

 

 

  final _costController = TextEditingController();

	CupertinoTextField(
          cursorColor: style.colors['main1'],
          placeholder: '금액을 입력해주세요',
          padding: EdgeInsets.all(13),
          controller: _costController,
          onChanged: onChanged,
          maxLines: 1,
          keyboardType: TextInputType.number,
          inputFormatters: [
              FilteringTextInputFormatter.allow(
                RegExp('[0-9]'),
              )
            ],
        ),

✔️ TitleInput은 CupertinoTextField를 직접 커스텀한 위젯입니다!

✔️ 0~9까지의 숫자만 허용하겠다는 의미! (allow 사용)

 

 

 

 

유효성을 검사할 수 있는 옵션이 존재하는가?
=> Form 기능 + TextField 기능 = TextFormField

 

 

 

Form위젯 사용하여 textField 상태관리 하는 방법

플러터에서 TextField를 입력 받으려면 기본적으로 TextEditingController를 사용해야 한다.

TextField가 하나라면 괜찮지만 많아질수록 컨트롤러 관리가 어려워진다 @_@

하지만 TextFormField를 사용하면 쉽게 validation과 값을 받아올 수 있음 ! 

 

 

 

기본 레이아웃

import 'package:flutter/material.dart';

class FormScreen extends StatefulWidget {
  @override
  _FormScreenState createState() => _FormScreenState();
}

class _FormScreenState extends State<FormScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [ 
          // 여기에 폼 작성하기
        ],
      ),
    );
  }
}

 

 

 

Form 위젯 사용하기

  final formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return DefaultAppbarLayout(
      child: Form(
        key: this.formKey,
        child: Column(
          children: [ 
            // 여기에 TextFormField들 입력하기!
          ],
        ),
      ),
    );
  }

✔️ Form 위젯은 childkey 파라미터를 받는다.

  • child에는 TextFormField들을 넣어준다
  • key에는 GlobalKey를 넣어주면 됨! 이 key는 나중에 폼 내부의 TextFormField 값들을 저장하고 validation을 진행하는데 사용된다.

 

 

 

TextFormField 위젯 생성 함수

  renderTextFormField({
    @required String label,
    @required FormFieldSetter onSaved,
    @required FormFieldValidator validator,
  }) {
    assert(onSaved != null);
    assert(validator != null);

    return Column(
      children: [
        Row(
          children: [
            Text(
              label,
              style: TextStyle(
                fontSize: 12.0,
                fontWeight: FontWeight.w700,
              ),
            ),
          ],
        ),
        TextFormField(
          onSaved: onSaved,
          validator: validator,
        ),
      ],
    );
  }

 

 

 

 

TextFormField의 onSaved, validator 파라미터

 @override
  Widget build(BuildContext context) {
    return DefaultAppbarLayout(
      child: Form(
        key: this.formKey,
        child: Padding(
          padding: EdgeInsets.all(16.0),
          child: Column(
            children: [
              renderTextFormField(
                label: '이름',
                onSaved: (val) {},
                validator: (val) {
                  return null;
                },
              ),
              renderTextFormField(
                label: '이메일',
                onSaved: (val) {},
                validator: (val) {
                  return null;
                },
              ),
              renderTextFormField(
                label: '비밀번호',
                onSaved: (val) {},
                validator: (val) {
                  return null;
                },
              ),
              renderTextFormField(
                label: '주소',
                onSaved: (val) {},
                validator: (val) {
                  return null;
                },
              ),
              renderTextFormField(
                label: '닉네임',
                onSaved: (val) {},
                validator: (val) {
                  return null;
                },
              ),
            ],
          ),
        ),
      ),
    );
  }

 

 

✔️ TextFormFieldonSavedvalidator 파라미터를 받는다.

  • onSaved의 시그니처는 FormFieldSetter 라는 typedef
  • validator의 시그니처는 FormFieldValidator
  • 둘 다 String을 받고 있고, validator는 String의 리턴값 또한 받는다. 리턴되는 String은 에러메시지로 사용된다

 

 

 

폼 저장 버튼 생성

 renderButton() {
    return RaisedButton(
      color: Colors.blue,
      onPressed: () {},
      child: Text(
        '저장하기!',
        style: TextStyle(
          color: Colors.white,
        ),
      ),
    );
  }

 

 

 

폼 저장 후 Snackbar 보여주기

  renderButton() {
    return RaisedButton(
      color: Colors.blue,
      onPressed: () async {
        if(this.formKey.currentState.validate()){
          // validation 이 성공하면 true 리턴!
          
          // validation 이 성공하면 폼 저장하기
          this.formKey.currentState.save();
          
          Get.snackbar(
            '저장완료!',
            '폼 저장이 완료되었습니다!',
            backgroundColor: Colors.white,
          );
        }

      },
      child: Text(
        '저장하기!',
        style: TextStyle(
          color: Colors.white,
        ),
      ),
    );
  }

✔️ formKey.currentState.validate() 함수를 실행하면 Form 내부에 있는 TextFormField들의 validation 결과에 따라 성공이면 true 리턴, 실패하면 false 리턴해준다.

 

 

 

Validator 파라미터 작성

⚠️ TextField에 아무것도 입력하지 않아도 저장이 된다. 모든 TextField의 validation 파라미터를 return null로 저장해서 그렇다. TextField 별로 적절한 에러 메시지 작성하기!

  @override
  Widget build(BuildContext context) {
    return DefaultAppbarLayout(
      child: Form(
        key: this.formKey,
        child: Padding(
          padding: EdgeInsets.all(16.0),
          child: Column(
            children: [
              renderTextFormField(
                label: '이름',
                onSaved: (val) {},
                validator: (val) {
                  if(val.length < 1) {
                    return '이름은 필수사항입니다.';
                  }

                  if(val.length < 2) {
                    return '이름은 두글자 이상 입력 해주셔야합니다.';
                  }

                  return null;
                },
              ),
              renderTextFormField(
                label: '이메일',
                onSaved: (val) {
                },
                validator: (val) {
                  if(val.length < 1) {
                    return '이메일은 필수사항입니다.';
                  }

                  if(!RegExp(
                      r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
                      .hasMatch(val)){
                    return '잘못된 이메일 형식입니다.';
                  }

                  return null;
                },
              ),
              renderTextFormField(
                label: '비밀번호',
                onSaved: (val) {},
                validator: (val) {
                  if(val.length < 1) {
                    return '비밀번호는 필수사항입니다.';
                  }

                  if(val.length < 8){
                    return '8자 이상 입력해주세요!';
                  }
                  return null;
                },
              ),
              renderTextFormField(
                label: '주소',
                onSaved: (val) {},
                validator: (val) {
                  if(val.length < 1) {
                    return '주소는 필수사항입니다.';
                  }
                  return null;
                },
              ),
              renderTextFormField(
                label: '닉네임',
                onSaved: (val) {},
                validator: (val) {
                  if(val.length < 1) {
                    return '닉네임은 필수사항입니다.';
                  }
                  if(val.length < 8) {
                    return '닉네임은 8자 이상 입력해주세요!';
                  }
                  return null;
                },
              ),
              renderButton(),
            ],
          ),
        ),
      ),
    );
  }

 

 

 

 

onSaved 파라미터 작성하기

  final formKey = GlobalKey<FormState>();

  String name = '';
  String email = '';
  String password = '';
  String address = '';
  String nickname = '';

  @override
  Widget build(BuildContext context) {
    return DefaultAppbarLayout(
      child: Form(
        key: this.formKey,
        child: Padding(
          padding: EdgeInsets.all(16.0),
          child: Column(
            children: [
              renderTextFormField(
                label: '이름',
                onSaved: (val) {
                  setState(() {
                    this.name = val;
                  });
                },
                validator: (val) {
                  if(val.length < 1) {
                    return '이름은 필수사항입니다.';
                  }

                  if(val.length < 2) {
                    return '이름은 두글자 이상 입력 해주셔야합니다.';
                  }

                  return null;
                },
              ),
              renderTextFormField(
                label: '이메일',
                onSaved: (val) {
                  setState(() {
                    this.email = val;
                  });
                },
                validator: (val) {
                  if(val.length < 1) {
                    return '이메일은 필수사항입니다.';
                  }

                  if(!RegExp(
                      r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
                      .hasMatch(val)){
                    return '잘못된 이메일 형식입니다.';
                  }

                  return null;
                },
              ),
              renderTextFormField(
                label: '비밀번호',
                onSaved: (val) {
                  setState(() {
                    this.password = val;
                  });
                },
                validator: (val) {
                  if(val.length < 1) {
                    return '비밀번호는 필수사항입니다.';
                  }

                  if(val.length < 8){
                    return '8자 이상 입력해주세요!';
                  }
                  return null;
                },
              ),
              renderTextFormField(
                label: '주소',
                onSaved: (val) {
                  setState(() {
                    this.address = val;
                  });
                },
                validator: (val) {
                  if(val.length < 1) {
                    return '주소는 필수사항입니다.';
                  }
                  return null;
                },
              ),
              renderTextFormField(
                label: '닉네임',
                onSaved: (val) {
                  setState(() {
                    this.nickname = val;
                  });
                },
                validator: (val) {
                  if(val.length < 1) {
                    return '닉네임은 필수사항입니다.';
                  }
                  if(val.length < 8) {
                    return '닉네임은 8자 이상 입력해주세요!';
                  }
                  return null;
                },
              ),
              renderButton(),
            ],
          ),
        ),
      ),
    );
  }

✔️ 값을 받아보기 위해 onSaved 파라미터에서 저장 시 위젯의 변수에 값을 저장하는 기능 작성! 

 

 

 

 

💡 꿀팁 ! 

TextFormFieldautovalidateModeAutovalidateMode.always로 지정하면, 저장하기 버튼을 누르기 전에 각 TextFormField가 자동으로 validation을 진행하는 것을 볼 수 있다! 

 

 

 

 

 

 

🌐 참고 링크

https://youtu.be/V4xk4pmzY2I

flutter_launcher_icons 패키지를 사용하면 앱 아이콘을 더 쉽게 변경할 수 있다!

 

 

1. 이미지 파일 준비하기

  • PNG 파일
  • 최대 1024KB
  • 1024px X 1024px 이상의 크기

📌 위 조건에 맞는 이미지를 assets/images/app_icon.png로 저장한다

 

 

 

2. flutter_launcher_icons 설치

아래 명령어를 실행하여 패키지를 설치한다

flutter pub add flutter_launcher_icons --dev

 

 

 

3. 앱 아이콘 설정하기

이미지 파일 설정하기 위해 최상단의 폴더에 flutter_launcher_icons.yaml 파일 생성하고 아래와 같이 작성한다

 

flutter_icons:
  ios: true
  android: true
  image_path: "assets/images/app_icon.png"
  remove_alpha_ios: true

 

 

 

4. 앱 아이콘 생성하기

아래 명령어를 실행하여 flutter_launcher_icons 패키지를 통해 앱 이미지 아이콘을 생성한다

flutter pub run flutter_launcher_icons:main

 

 

 

 

5. 확인

앱을 삭제하고 프로젝트를 다시 실행시켰더니 , 앱 아이콘이 잘 변경된 것을 확인할 수 있다!

 

 

 

AppBar에서 leading 설정을 하지 않았는데도
뒤로가기 버튼이 표시되는 경우가 있다.

 

이럴경우,

AppBar의 속성 중에 automaticallyImplyLeading 속성을 false로 지정하면 됨! (기본값이 true 임)

 

 

👇 예시 코드

class SecondScreen extends StatelessWidget {
  const SecondScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: false,
        title: const Text('Second Screen'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('Go Back'),
        ),
      ),
    );
  }
}

 

⏬ automaticallyImplyLeading: false 적용을 하지 않는다면 기본 값이 true이기 때문에 아래와 같이 뒤로가기 버튼이 생성되어 있는 것을 볼 수 있다.

 

false로 지정해주면, 뒤로가기 버튼이 사라져 있는 것을 볼 수 있음!

 

 

 

 

 

🌐 공식 문서

https://api.flutter.dev/flutter/material/AppBar/automaticallyImplyLeading.html

+ Recent posts

loading