[Flutter] 스나이퍼팩토리 33일차

KWANWOO·2023년 3월 10일
1
post-thumbnail

스나이퍼팩토리 플러터 33일차

33일차에서는 지금까지 배운 내용을 적용해 비밀듣는 고양이 앱을 MVC 패턴으로 만들어 보았다.

학습한 내용

  • 비밀듣는 고양이 앱 제작(최종)

추가 내용 정리

Get.lazyPut의 fenix

앱을 생성할 때 모든 컨트롤러를 등록하고 사용할 필요는 없다. 해당 컨트롤러가 사용될 때 등록을 하고 사용하면 되는데 이를 Get.lazyPut으로 할 수 있다.

Get.lazyPut을 하면 컨트롤러 인스턴스를 바로 만들지 않고, Get.find를 할 때 생성한다.

하지만 코드를 작성하면서 initialBindind에서 Get.lazyPut을 사용하고, 페이지 이동을 할 때, Get.offNamed()등을 사용할 때가 있는데 이 경우에는 해당 페이지가 스택에서 제거되면서 컨트롤러도 dispose되어 다시 페이지에 들어가면 에러가 발생한다.

따라서 Get.lazyPutfenix 속성을 true로 설정하면 이전에 컨트롤러가 dispose 되어도 다시 사용이 가능하게 할 수 있어 에러를 해결할 수 있다.


33일차 과제

  1. 비밀듣는 고양이 앱 제작(최종)

1. 비밀듣는 고양이 앱 제작(최종)

지금까지 비밀듣는 고양이 앱을 총 두 가지 버전으로 제작했었다.

이번에는 비밀듣는 고양이 앱을 pub.dev의 패키지를 사용하지 않고 직접 API URL에 요청을 보내 데이터를 받아 만들었다.

또한 GetX를 사용하여 상태관리를 수행한다.

요구사항

  1. 비밀듣는 고양이를 secret_cat_sdk를 사용하지 않고 제작하시오.
  2. 주어진 API 명세서를 보고, 플러터에서 앱을 제작하시오.
  3. 아래의 필수 기능을 포함하시오.
    • 로그인, 회원가입 기능
    • 유저 인증상태가 바뀌면 자동으로 페이지 리다이렉트 기능
    • 디자인은 직접 수행할 것

API 명세서

Users

{
  "page": 1,
  "perPage": 30,
  "totalPages": 1,
  "totalItems": 2,
  "items": [
    {
      "id": "**USER_RECORD_ID** (주말과제시 필요)",
      "collectionId": "_pb_users_auth_",
      "collectionName": "users",
      "created": "2022-01-01 01:00:00Z",
      "updated": "2022-01-01 23:59:59Z",
      "username": "username123",
      "verified": false,
      "emailVisibility": true,
      "email": "test@example.com",
      "name": "test",
      "avatar": "filename.jpg"
    }
  ]
}
{
  "token": "JWT_TOKEN",
  "record": {
    "id": "RECORD_ID",
    "collectionId": "_pb_users_auth_",
    "collectionName": "users",
    "created": "2022-01-01 01:00:00Z",
    "updated": "2022-01-01 23:59:59Z",
    "username": "username123",
    "verified": false,
    "emailVisibility": true,
    "email": "test@example.com",
    "name": "test",
    "avatar": "filename.jpg"
  }
}
  • SignUp 회원가입
    • POST http://52.79.115.43:8090/api/collections/users/records
    • Request
      • email (String - required, 올바른 이메일 형식일 것)
      • password (String - required, 9글자 이상)
      • passwordConfirm (String - required, 9글자 이상)
      • username (String - required, 9글자 이상)
    • Success Response
{
  "id": "RECORD_ID",
  "collectionId": "_pb_users_auth_",
  "collectionName": "users",
  "created": "2022-01-01 01:00:00Z",
  "updated": "2022-01-01 23:59:59Z",
  "username": "username123",
  "verified": false,
  "emailVisibility": true,
  "email": "test@example.com",
  "name": "test",
  "avatar": "filename.jpg"
}

Secrets

{
  "page": 1,
  "perPage": 30,
  "totalPages": 1,
  "totalItems": 2,
  "items": [
    {
      "id": "RECORD_ID",
      "collectionId": "5647cebjvtwtcu1",
      "collectionName": "secrets",
      "created": "2022-01-01 01:00:00Z",
      "updated": "2022-01-01 23:59:59Z",
      "secret": "test",
      "author": "RELATION_RECORD_ID",
			"authorName": "test",
    }
  ]
}
{
  "id": "RECORD_ID",
  "collectionId": "5647cebjvtwtcu1",
  "collectionName": "secrets",
  "created": "2022-01-01 01:00:00Z",
  "updated": "2022-01-01 23:59:59Z",
  "secret": "test",
  "author": "RELATION_RECORD_ID",
	"authorName":"test"
}

코드 작성

  • pubspec.yaml
dependencies:
  cupertino_icons: ^1.0.2
  dio: ^5.0.1
  flutter:
    sdk: flutter
  font_awesome_flutter: ^10.4.0
  get: ^4.6.5

pubspec.yaml에 사용할 패키지들을 설치했다.

  • lib/main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret_app/controller/auth_controller.dart';
import 'package:secret_app/controller/login_controller.dart';
import 'package:secret_app/controller/secret_controller.dart';
import 'package:secret_app/controller/signup_controller.dart';
import 'package:secret_app/controller/upload_controller.dart';
import 'package:secret_app/util/app_pages.dart';
import 'package:secret_app/util/app_routes.dart';

void main() {
  runApp(const SecretApp());
}

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

  
  Widget build(BuildContext context) {
    return GetMaterialApp(
      //컨트롤러 바인딩
      initialBinding: BindingsBuilder(() {
        Get.put(AuthController());
        Get.lazyPut(() => LoginController(), fenix: true);
        Get.lazyPut(() => SignupController(), fenix: true);
        Get.lazyPut(() => SecretController(), fenix: true);
        Get.lazyPut(() => UploadController(), fenix: true);
      }),
      getPages: AppPages.pages,
      initialRoute: AppRoutes.login, //로그인 페이지 호출
    );
  }
}

main.dart에서는 컨트롤러들을 바인딩하고, 페이지 라우트를 설정했다. 초기 라우트로는 로그인 페이지를 띄웠다.

  • lib/util/network.dart
import 'package:dio/dio.dart';

class Network {
  //dio 객체 생성
  static final dio = Dio(
    BaseOptions(
      baseUrl: 'http://52.79.115.43:8090/',
    ),
  );
}

Network에 앱에서 사용할 dio 객체를 생성했다. 여기서 baseUrl을 설정했다.

  • lib/util/api_routes.dart
class ApiRoutes {
  static const user = 'api/collections/users/records?sort=-created'; //GET
  static const authWithPassword =
      'api/collections/users/auth-with-password'; //POST
  static const signup = 'api/collections/users/records'; //POST
  static const getSecrets =
      'api/collections/secrets/records?sort=-created'; //GET
  static const uploadSecret = 'api/collections/secrets/records'; //POST
}

ApiRoutes에 사용할 API의 라우트들을 저장했다.

  • lib/util/app_routes.dart
import 'package:secret_app/view/page/login_page.dart';
import 'package:secret_app/view/page/main_page.dart';
import 'package:secret_app/view/page/secret_page.dart';
import 'package:secret_app/view/page/setting_page.dart';
import 'package:secret_app/view/page/signup_page.dart';
import 'package:secret_app/view/page/upload_page.dart';

class AppRoutes {
  static const main = MainPage.route; //메인페이지
  static const login = LoginPage.route; //로그인 페이지
  static const signup = SignupPage.route; //회원가입 페이지
  static const secret = SecretPage.route; //비밀 페이지
  static const upload = UploadPage.route; //비밀 업로드 페이지
  static const setting = SettingPage.route; //설정 페이지
}

AppRoutes에 각 페이지들의 라우트를 저장했다.

  • lib/util/app_pages.dart
import 'package:get/get.dart';
import 'package:secret_app/view/page/login_page.dart';
import 'package:secret_app/view/page/main_page.dart';
import 'package:secret_app/view/page/secret_page.dart';
import 'package:secret_app/view/page/setting_page.dart';
import 'package:secret_app/view/page/signup_page.dart';
import 'package:secret_app/view/page/upload_page.dart';

import 'app_routes.dart';

class AppPages {
  //페이지 라우팅
  static final pages = [
    GetPage(name: AppRoutes.main, page: () => const MainPage()),
    GetPage(name: AppRoutes.login, page: () => const LoginPage()),
    GetPage(name: AppRoutes.signup, page: () => const SignupPage()),
    GetPage(name: AppRoutes.secret, page: () => const SecretPage()),
    GetPage(name: AppRoutes.upload, page: () => const UploadPage()),
    GetPage(name: AppRoutes.setting, page: () => const SettingPage()),
  ];
}

AppPages에 페이지 라우트를 설정한 pages를 저장했다.

  • lib/model/user.dart
// ignore_for_file: public_member_api_docs, sort_constructors_first
class User {
  String id; //아이디
  String collectionId; //컬렉션 아이디
  String collectionName; //컬렉션 이름
  DateTime created; //생성 날짜
  DateTime updated; //업데이트 날짜
  String username; //유저 이름
  bool verified; //인증 여부
  bool emailVisibility; //이미지 가시성
  String email; //이메일
  String? name; //이름
  String? avatar; //아바타

  User({
    required this.id,
    required this.collectionId,
    required this.collectionName,
    required this.created,
    required this.updated,
    required this.username,
    required this.verified,
    required this.emailVisibility,
    required this.email,
    this.name,
    this.avatar,
  });

  factory User.fromMap(Map<String, dynamic> map) {
    return User(
      id: map['id'] as String,
      collectionId: map['collectionId'] as String,
      collectionName: map['collectionName'] as String,
      created: DateTime.parse(map['created']),
      updated: DateTime.parse(map['updated']),
      username: map['username'] as String,
      verified: map['verified'] as bool,
      emailVisibility: map['emailVisibility'] as bool,
      email: map['email'] as String,
      name: map['name'] != '' ? map['name'] as String : null,
      avatar: map['avatar'] != '' ? map['avatar'] as String : null,
    );
  }
}

User는 사용자의 정보를 가지는 클래스 모델이다.

  • lib/model/secret.dart
// ignore_for_file: public_member_api_docs, sort_constructors_first
class Secret {
  String id; //아이디
  String collectionId; //컬렉션 아이디
  String collectionName; //컬렉션 이름
  DateTime created; //생성 날짜
  DateTime updated; //업데이트 날짜
  String secret; //비밀 텍스트
  String? author; //작성자(user record id)
  String? authorName; //작성자이름(닉네임)

  Secret({
    required this.id,
    required this.collectionId,
    required this.collectionName,
    required this.created,
    required this.updated,
    required this.secret,
    this.author,
    this.authorName,
  });

  factory Secret.fromMap(Map<String, dynamic> map) {
    return Secret(
      id: map['id'] as String,
      collectionId: map['collectionId'] as String,
      collectionName: map['collectionName'] as String,
      created: DateTime.parse(map['created']),
      updated: DateTime.parse(map['updated']),
      secret: map['secret'] as String,
      author: map['author'] != '' ? map['author'] as String : null,
      authorName: map['authorName'] != '' ? map['authorName'] as String : null,
    );
  }
}

Secret은 비밀의 정보를 가지는 클래스 모델이다.

  • lib/controller/auth_controller.dart
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:secret_app/model/user.dart';
import 'package:secret_app/util/api_routes.dart';
import 'package:secret_app/util/app_routes.dart';
import 'package:secret_app/util/network.dart';

class AuthController extends GetxController {
  final Rxn<User> _user = Rxn(); //유저 정보

  User? get user => _user.value; //user 정보 읽기

  //로그인
  login(String id, String pw) async {
    try {
      var res = await Network.dio.post(ApiRoutes.authWithPassword,
          data: {
            'identity': id,
            'password': pw,
          },
          options: Options(contentType: Headers.formUrlEncodedContentType));

      if (res.statusCode == 200) {
        var user = User.fromMap(res.data['record']);
        _user(user); //유저 정보 저장
      }
    } on DioError catch (e) {
      print(e.message);
    }
  }

  //로그 아웃
  logout() {
    _user.value = null; //유저 정보 삭제
  }

  //회원 가입
  signup(
    String email,
    String password,
    String passwordConfirm,
    String username,
  ) async {
    try {
      await Network.dio.post(
        ApiRoutes.signup,
        data: {
          'email': email,
          'password': password,
          'passwordConfirm': passwordConfirm,
          'username': username,
        },
      );
    } on DioError catch (e) {
      print(e);
    }
  }

  //유저 정보에 따른 페이지 이동
  _handleAuthChanged(User? data) {
    //유저 정보가 있으면 메인페이지로 이동
    if (data != null) {
      Get.offNamed(AppRoutes.main);
      return;
    }
    //유저 정보가 없으면 로그인 페이지로 이동
    Get.offAllNamed(AppRoutes.login);
    return;
  }

  
  void onInit() {
    super.onInit();
    //유저 정보를 관찰하여 변경된 경우 실행
    ever(_user, _handleAuthChanged);
  }
}

AuthController는 유저의 정보를 가지는 전역 컨트롤러이다. 로그인을 수행하는 login() 로그아웃을 수행하는 logout() 회원가입을 수행하는 signup() 메소드를 작성했다.

유저의 정보가 있으면 화면이 자동으로 메인 페이지로 이동하고 없으면 로그인 페이지로 이동하도록 onInitever를 사용했다.

  • lib/controller/login_controller.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret_app/controller/auth_controller.dart';

class LoginController extends GetxController {
  var idController = TextEditingController(); //id 텍스트 필드 컨트롤러
  var pwController = TextEditingController(); //패스워드 텍스트 필트 컨트롤러

  //AuthController의 login()을 사용해 로그인 수행
  login() {
    Get.find<AuthController>().login(idController.text, pwController.text);
  }
}

LoginController는 로그인 페이지에서 아이디와 패스워드 입력 폼을 연결한 컨트롤러를 가지고 있다.

login() 메소드는 AuthControllerlogin()을 사용해 로그인을 수행한다.

  • lib/controller/signup_controller.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret_app/controller/auth_controller.dart';

class SignupController extends GetxController {
  var emailController = TextEditingController(); //id 텍스트 필드 컨트롤러
  var pwController = TextEditingController(); //패스워드 텍스트 필트 컨트롤러
  var pwConfirmController = TextEditingController(); //패스워드 확인 텍스트 필트 컨트롤러
  var userNameController = TextEditingController(); //닉네임 텍스트 필트 컨트롤러

  RxString errorMsg = ''.obs;

  //AuthController의 signup()을 사용해 로그인 수행
  Future<bool> signup() async {
    //이메일 형식 체크
    if (!RegExp(
            r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+")
        .hasMatch(emailController.text)) {
      errorMsg('이메일 형식이 잘못되었습니다.');
      return false;
    }
    //비밀번호 글자 수 체크
    if (pwController.text.length < 9) {
      errorMsg('비밀번호는 9자리 이상이어야 합니다.');
      return false;
    }
    //비밀번호와 비밀번호 확인을 체크
    if (pwConfirmController.text != pwController.text) {
      errorMsg('비밀번호와 비밀번호 확인이 다릅니다.');
      return false;
    }
    //username이 null인지 체크
    if (userNameController.text == '') {
      errorMsg('닉네임을 입력하세요.');
      return false;
    }

    await Get.find<AuthController>().signup(
      emailController.text,
      pwController.text,
      pwConfirmController.text,
      userNameController.text,
    );

    return true;
  }
}

SignupController는 회원가입 페이지의 네 가지 요소의 TextEditingController를 가지고 있으며, 각 요소의 형식을 체크하고 결과를 저장할 errorMsg를 가진다.

signup()에서는 이메일, 비밀번호, 닉네임을 확인해 형식이 맞은 경우에 AuthControllersignup()을 호출한다.

  • lib/controller/secret_controller.dart
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:secret_app/model/secret.dart';
import 'package:secret_app/util/api_routes.dart';
import 'package:secret_app/util/network.dart';

class SecretController extends GetxController {
  RxList<Secret> secrets = RxList();

  readSecrets() async {
    try {
      //네트워크 데이터 요청(비밀 리스트)
      var res = await Network.dio.get(ApiRoutes.getSecrets);

      if (res.statusCode == 200) {
        List<Map<String, dynamic>> data =
            List<Map<String, dynamic>>.from(res.data['items']);

        //data serialization 후 저장
        secrets.addAll(data.map((e) => Secret.fromMap(e)).toList());
      }
    } on DioError catch (e) {
      print(e);
    }
  }

  
  void onInit() {
    super.onInit();
    readSecrets(); //비밀 데이터 가져오기
  }
}

SecretController는 비밀 리스트를 저장하고 있다. 비밀리스트를 불러오는 readSecrets를 작성하고, onInit()에서 호출했다.

  • lib/controller/upload_controller.dart
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret_app/controller/auth_controller.dart';
import 'package:secret_app/util/api_routes.dart';
import 'package:secret_app/util/network.dart';

class UploadController extends GetxController {
  var textController = TextEditingController();
  String resultMsg = '';
  RxBool isAnonymous = false.obs;

  //비밀 업로드
  uploadSecret() async {
  	if (textController.text == '') return;
  
    var controller = Get.find<AuthController>();
    try {
      var res = await Network.dio.post(
        ApiRoutes.uploadSecret,
        data: {
          'secret': textController.text,
          'author': controller.user!.id,
          'authorName': !isAnonymous.value ? controller.user!.username : ''
        },
      );

      if (res.statusCode == 200) {
        resultMsg = '비밀을 성공적으로 업로드했습니다.';
      }
    } on DioError catch (e) {
      resultMsg = '[ERROR]비밀 업로드에 실패하였습니다.';
      print(e);
    }
  }

  checkAnonymous(bool? value) {
    if (value != null) {
      isAnonymous(value);
    }
  }
}

UploadController는 업로드할 텍스트 필드의 컨트로러와 결과 메세지를 가지며, 익명으로 할지 여부를 판단할 isAnonymous를 저장한다.

비밀을 업로드하는 메소드는 uploadSecret()으로 작성했고, 업로드에 성공하면 성공 메세지를 resultMsg에 저장한다.

  • lib/widget/app_logo.dart
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';

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

  
  Widget build(BuildContext context) {
    return const Column(
      children: [
        Icon(
          size: 40,
          color: Colors.redAccent,
          FontAwesomeIcons.cat,
        ),
        Text(
          style: TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
          '비밀듣는 고양이',
        ),
      ],
    );
  }
}

앱에서 자주 사용할 로고를 커스텀 위젯으로 작성했다.

  • lib/widget/custom_text_field.dart
import 'package:flutter/material.dart';

class CustomTextField extends StatelessWidget {
  const CustomTextField(
      {super.key,
      this.maxLines,
      required this.hintText,
      required this.controller});

  final int? maxLines; //최대 라인 수
  final String hintText; //힌트 텍스트
  final TextEditingController controller; //컨트롤러

  
  Widget build(BuildContext context) {
    return TextField(
      controller: controller,
      maxLines: maxLines ?? 1,
      cursorColor: Colors.redAccent,
      decoration: InputDecoration(
        focusedBorder: const OutlineInputBorder(
          borderSide: BorderSide(
            color: Colors.redAccent,
          ),
        ),
        focusColor: Colors.redAccent,
        border: const OutlineInputBorder(
          borderSide: BorderSide(),
        ),
        hintText: hintText,
      ),
    );
  }
}

앱에서 사용할 텍스트 필드를 커스텀 위젯으로 만들었다.

  • lib/widget/custom_button
import 'package:flutter/material.dart';

class CustomButton extends StatelessWidget {
  const CustomButton(
      {super.key, required this.text, required this.onPressed, this.margin});

  final String text; //출력 텍스트
  final VoidCallback onPressed; //onPressed 이벤트 핸들러
  final EdgeInsets? margin; //버튼의 마진

  
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: margin,
      child: ElevatedButton(
        onPressed: onPressed,
        style: ElevatedButton.styleFrom(
          backgroundColor: Colors.redAccent,
        ),
        child: Text(
          style: const TextStyle(
            fontSize: 16,
          ),
          text,
        ),
      ),
    );
  }
}

앱에서 사용할 버튼을 커스텀 위젯으로 만들었다.

  • lib/page/login_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret_app/controller/login_controller.dart';
import 'package:secret_app/util/app_routes.dart';
import 'package:secret_app/view/widget/app_logo.dart';
import 'package:secret_app/view/widget/custom_button.dart';
import 'package:secret_app/view/widget/custom_text_field.dart';

class LoginPage extends GetView<LoginController> {
  const LoginPage({super.key});
  static const route = '/login';

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const AppLogo(),
          const SizedBox(height: 16),
          //아이디 텍스트 필드
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: CustomTextField(
              hintText: '아이디',
              controller: controller.idController,
            ),
          ),
          //패스워드 텍스트 필드
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: CustomTextField(
              hintText: '비밀번호',
              controller: controller.pwController,
            ),
          ),
          CustomButton(
            margin: const EdgeInsets.all(8.0),
            text: '로그인',
            onPressed: () {
              controller.login();
            },
          ),
          //회원가입 페이지 이동
          TextButton(
            onPressed: () => Get.toNamed(AppRoutes.signup),
            style: TextButton.styleFrom(
              foregroundColor: Colors.redAccent,
            ),
            child: const Text('회원가입'),
          ),
        ],
      ),
    );
  }
}

LoginPage는 로그인을 수행하는 페이지로 아이디와 패스워드의 텍스트 필드를 가지고, 로그인 버튼을 넣었다.

회원가입을 누르면 회원가입 페이지로 이동한다.

  • lib/page/signup_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret_app/controller/signup_controller.dart';
import 'package:secret_app/view/widget/app_logo.dart';
import 'package:secret_app/view/widget/custom_button.dart';
import 'package:secret_app/view/widget/custom_text_field.dart';

class SignupPage extends GetView<SignupController> {
  const SignupPage({super.key});
  static const route = '/signup';

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: SingleChildScrollView(
          physics: const BouncingScrollPhysics(),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Padding(
                padding: EdgeInsets.all(16.0),
                child: AppLogo(),
              ),
              //이메일 입력 필드
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: CustomTextField(
                  hintText: '이메일',
                  controller: controller.emailController,
                ),
              ),
              //비밀번호 입력 필드
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: CustomTextField(
                  hintText: '비밀번호',
                  controller: controller.pwController,
                ),
              ),
              //비밀번호 확인 입력 필드
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: CustomTextField(
                  hintText: '비밀번호 확인',
                  controller: controller.pwConfirmController,
                ),
              ),
              //닉네임 입력 필드
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: CustomTextField(
                  hintText: '닉네임',
                  controller: controller.userNameController,
                ),
              ),
              //입력 형식 오류 메세지
              Obx(
                () => Text(
                  style: const TextStyle(
                    color: Colors.redAccent,
                  ),
                  controller.errorMsg.value,
                ),
              ),
              //회원가입 버튼
              CustomButton(
                margin: const EdgeInsets.all(8.0),
                text: '회원가입',
                onPressed: () async {
                  if (await controller.signup()) {
                    Get.back();
                  }
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

SignupPage는 회원가입을 수행하는 페이지로, 이메일, 패스워드, 패스워드 확인, 닉네임 텍스트 필드를 가진다.

회원가입 버튼을 누르면 회원가입을 수행하고, 페이지를 뒤로 이동한다.

입력 형식이 맞지 않으면 오류 메세지를 출력한다.

  • lib/page/main_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret_app/util/app_routes.dart';
import 'package:secret_app/view/widget/app_logo.dart';

class MainPage extends StatelessWidget {
  const MainPage({super.key});
  static const route = '/main';

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const AppLogo(),
          const SizedBox(height: 12),
          //비밀페이지로 이동
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: ListTile(
              tileColor: Colors.black12,
              title: const Text('비밀 보기'),
              subtitle: const Text('모든 비밀을 확인하기'),
              trailing: const Icon(Icons.navigate_next),
              onTap: () => Get.toNamed(AppRoutes.secret),
            ),
          ),
          //작성자들 페이지로 이동
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: ListTile(
              tileColor: Colors.black12,
              title: const Text('비밀 만들기'),
              subtitle: const Text('나의 비밀 작성하기'),
              trailing: const Icon(Icons.navigate_next),
              onTap: () => Get.toNamed(AppRoutes.upload),
            ),
          ),
          //비밀 업로드 페이지로 이동
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: ListTile(
              tileColor: Colors.black12,
              title: const Text('설정'),
              subtitle: const Text('내 정보 설정하기'),
              trailing: const Icon(Icons.navigate_next),
              onTap: () => Get.toNamed(AppRoutes.setting),
            ),
          ),
        ],
      ),
    );
  }
}

MainPage는 세 가지 페이지로 이동할 수 있는 리스트 타일을 출력한다. 각 리스트 타일을 누르면 해당 페이지로 이동한다.

  • lib/page/secret_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret_app/controller/secret_controller.dart';
import 'package:secret_app/view/widget/app_logo.dart';

class SecretPage extends GetView<SecretController> {
  const SecretPage({super.key});
  static const route = '/secret';

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        decoration: const BoxDecoration(
          //배경 이미지
          image: DecorationImage(
            image: NetworkImage(
              'https://cdn.pixabay.com/photo/2018/07/31/20/27/silhouette-3575860_960_720.png',
            ),
            fit: BoxFit.cover,
            colorFilter: ColorFilter.mode(Colors.white70, BlendMode.lighten),
          ),
        ),
        child: Obx(
          //비밀들 페이지 뷰
          () => PageView.builder(
            physics: const BouncingScrollPhysics(),
            itemCount: controller.secrets.length,
            itemBuilder: (context, index) {
              var currentSecret = controller.secrets[index];
              return Center(
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    const AppLogo(), //앱 로고
                    const SizedBox(height: 16),
                    //비밀 텍스트
                    Text(
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                      textAlign: TextAlign.center,
                      currentSecret.secret,
                    ),
                    const SizedBox(height: 16),
                    //작성자
                    Text(
                      currentSecret.authorName ?? '익명',
                    )
                  ],
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

SecretPage는 비밀들을 페이지 뷰로 출력한다.

  • lib/upload_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret_app/controller/upload_controller.dart';
import 'package:secret_app/view/widget/app_logo.dart';
import 'package:secret_app/view/widget/custom_button.dart';
import 'package:secret_app/view/widget/custom_text_field.dart';

class UploadPage extends GetView<UploadController> {
  const UploadPage({super.key});
  static const route = '/upload';

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const AppLogo(), //앱 로고
            const SizedBox(height: 16),
            //비밀 입력 텍스트 필드
            CustomTextField(
              maxLines: 6,
              hintText: '비밀을 입력하세요.',
              controller: controller.textController,
            ),
            //익명 체크박스
            Row(
              children: [
                Obx(
                  () => Checkbox(
                    activeColor: Colors.redAccent,
                    value: controller.isAnonymous.value,
                    onChanged: controller.checkAnonymous,
                  ),
                ),
                const Text('익명'),
              ],
            ),
            //비밀 업로드 버튼
            CustomButton(
              text: '비밀 업로드',
              onPressed: () async {
                await controller.uploadSecret();
                if (controller.textController.text != '') {
                  Get.back();
                  Get.snackbar('비밀 업로드', controller.resultMsg);
                }
              },
            ),
          ],
        ),
      ),
    );
  }
}

UploadPage는 입력한 텍스트를 비밀로 서버에 업로드한다. 익명 체크 박스를 선택하면 익명으로 비밀을 올릴 수 있다.

비밀 업로드 버튼을 누르면 페이지가 뒤로 이동하며, 성공 또는 실패 메세지를 스낵바로 출력한다.

  • lib/page/setting_page.dart
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:secret_app/controller/auth_controller.dart';
import 'package:secret_app/view/widget/app_logo.dart';

class SettingPage extends GetView<AuthController> {
  const SettingPage({super.key});
  static const route = '/setting';

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            children: [
              const AppLogo(), //앱 로고
              const SizedBox(height: 16),
              //유저 정보 출력
              ListTile(
                title: Text(controller.user!.username),
                subtitle: const Text('안녕하세요'),
                leading: CircleAvatar(
                  backgroundColor: Colors.redAccent,
                  child: Text(
                    style: const TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold,
                    ),
                    controller.user!.username[0],
                  ),
                ),
              ),
              //리스트 타일을 누르면 로그아웃
              ListTile(
                title: const Text('로그아웃'),
                leading: const Icon(FontAwesomeIcons.rightFromBracket),
                onTap: controller.logout,
              )
            ],
          ),
        ),
      ),
    );
  }
}

SettingPage는 로그인 된 유저의 정보를 출력하며, 로그아웃 리스트 타일을 누르면 로그아웃이 되어 로그인 페이지로 이동된다.

결과

  • 회원가입 기능

  • 로그인과 설정페이지의 로그아웃 기능

  • 비밀 업로드와 비밀 리스트 페이지


이번주도 끝났다.

오늘은 지금까지 배웠던 내용들을 종합해서 비밀듣는 고양이 앱을 만들었다. 코드를 나름 깔끔하게 잘 쓴 것 같다. 근데 파일이 21개가 만들어져서 블로그 쓰는데 좀 오래 걸렸다.ㅠㅠ 추가 내용 정리는 새롭게 학습한 내용은 없고 과제에서 사용한 Get.lazyPut의 fenix 속성만 간단히 적었다. 이제 테디님 강의 보고 학습만 하면 된다.ㅋㅋㅋㅋ 포스팅은 여기서 끝!!!! (앱 하나 만들었는데 벌써 오후 4시네...ㅋㅋ)

📄Reference

profile
관우로그

0개의 댓글

관련 채용 정보