AWS 시작하기

Flutter 애플리케이션 구축

AWS Amplify를 사용하여 간단한 Flutter 애플리케이션 생성

모듈 1: Flutter 앱 생성 및 배포

이 모듈에서는 AWS Amplify의 웹 호스팅 서비스를 사용하여 Flutter 애플리케이션을 만들어 클라우드에 배포합니다.

소개

AWS Amplify는 코드 몇 줄만으로 사용자 인증, 파일 저장, 분석 이벤트 캡처 등을 수행할 수 있는 간편한 라이브러리를 제공하여 개발자가 더 빠르게 앱을 구축할 수 있도록 지원하는 도구 제품군입니다.

이 모듈에서는 사진 갤러리 앱의 UI를 생성하고 구축합니다. 여기에는 등록 흐름, 이미지 보기를 위한 갤러리 페이지, 카메라로 사진을 찍는 기능이 포함됩니다.

이 모듈에서는 다음 모듈에서 특정 범주에 대한 실제 Amplify 구현에 집중할 수 있도록 앱의 기반을 설정합니다.

배우게 될 내용

  • 등록 및 로그인 흐름 구현
  • 화면 간 탐색
  • 위젯의 그리드 구현
  • 디바이스 카메라로 사진 촬영

주요 개념

Navigator - 이 자습서에서는 Float Navigator 2.0을 사용합니다. Navigator는 페이지 목록을 사용하여 선언적 구현을 통해 표시할 보기를 결정합니다.

콜백 - 한 객체에서 다른 객체로 데이터를 전송하기 위해 통신하는 데 콜백을 사용합니다. 콜백은 콜 사이트에서 인수를 전달할 수 있다는 점에서 함수와 유사하지만, 다른 곳에서 코드를 실행합니다.

 완료 시간

30분

 사용되는 서비스

구현

  • Flutter 프로젝트 생성

    Visual Studio Code를 시작하고 원하는 이름으로 새 Flutter 프로젝트를 생성합니다.

    FlutterApp-Module1Photo1-small

    프로젝트가 준비되면 main.dart의 상용 코드를 다음 코드로 바꿉니다.

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    // 1
    class MyApp extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _MyAppState();
    }
    
    class _MyAppState extends State<MyApp> {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Photo Gallery App',
          theme: ThemeData(visualDensity: VisualDensity.adaptivePlatformDensity),
          // 2
          home: Navigator(
            pages: [],
            onPopPage: (route, result) => route.didPop(result),
          ),
        );
      }
    }
    1. MyApp 위젯을 StatefulWidget으로 변경했습니다. 나중에 이 위젯의 상태를 조작할 것입니다.
    2. MaterialApp의 홈 위젯은 선언적인 방식으로 탐색을 설정할 수 있는 Navigator입니다.
  • 인증 흐름 생성
    Navigator에 페이지를 추가하려면 먼저 각 페이지를 나타낼 위젯을 생성해야 합니다. 먼저 로그인 페이지에 login_page.dart라는 새 파일을 넣습니다.
    import 'package:flutter/material.dart';
    
    class LoginPage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _LoginPageState();
    }
    
    class _LoginPageState extends State<LoginPage> {
      // 1
      final _usernameController = TextEditingController();
      final _passwordController = TextEditingController();
    
      @override
      Widget build(BuildContext context) {
        // 2
        return Scaffold(
          // 3
          body: SafeArea(
              minimum: EdgeInsets.symmetric(horizontal: 40),
              // 4
              child: Stack(children: [
                // Login Form
                _loginForm(),
    
                // 6
                // Sign Up Button
                Container(
                  alignment: Alignment.bottomCenter,
                  child: FlatButton(
                      onPressed: () {},
                      child: Text('Don\'t have an account? Sign up.')),
                )
              ])),
        );
      }
    
      // 5
      Widget _loginForm() {
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Username TextField
            TextField(
              controller: _usernameController,
              decoration:
                  InputDecoration(icon: Icon(Icons.mail), labelText: 'Username'),
            ),
    
            // Password TextField
            TextField(
              controller: _passwordController,
              decoration: InputDecoration(
                  icon: Icon(Icons.lock_open), labelText: 'Password'),
              obscureText: true,
              keyboardType: TextInputType.visiblePassword,
            ),
    
            // Login Button
            FlatButton(
                onPressed: _login,
                child: Text('Login'),
                color: Theme.of(context).accentColor)
          ],
        );
      }
    
      // 7
      void _login() {
        final username = _usernameController.text.trim();
        final password = _passwordController.text.trim();
    
        print('username: $username');
        print('password: $password');
      }
    }
    1. LoginPage에는 사용자 입력이 필요하므로 화면의 각 필드에 TextEditingController를 사용하여 해당 상태를 추적해야 합니다. 이 예의 경우 사용자 이름과 암호를 추적합니다.
    2. _LoginPageState.build는 모바일 디바이스에 맞게 위젯을 포맷할 수 있는 Scaffold를 반환합니다.
    3. 이 앱은 여러 디바이스에서 실행될 수 있으므로 SafeArea를 관찰하는 것이 중요합니다. 이 경우 최소 가장자리 인셋을 활용하여 화면 왼쪽과 오른쪽 양쪽에 채우기를 추가하여 로그인 양식이 가장자리에서 가장자리까지 표시되지 않도록 합니다.
    4. UI는 기본 로그인 양식과 사용자가 로그인하는 대신 등록할 수 있는 화면 하단의 버튼으로 구성됩니다. 여기서는 각 하위 위젯의 배치를 쉽게 조작할 수 있도록 스택을 사용합니다.
    5. _loginForm 함수를 생성하는 것은 전적으로 선택 사항이지만, build 메서드를 약간 정리합니다. 여기서는 사용자 이름 및 암호 텍스트 필드와 로그인 버튼의 UI를 구현합니다.
    6. 외부 등록 버튼은 사용자가 계정이 없는 경우 등록할 수 있는 대화형 문장의 형태를 취합니다. 아직 구현되지 않은 onPressed 기능이 없습니다.
    7. _login 메서드는 텍스트 필드 컨트롤러에서 값을 추출하고 AuthCredentials 객체를 생성하는 역할을 합니다. 지금은 단순히 각 컨트롤러의 값을 인쇄합니다.

    LoginPage의 UI가 완성되지 않았습니다. main.dart의 Navigator에 추가하겠습니다.

    ... // home: Navigator(
    
    pages: [MaterialPage(child: LoginPage())],
    
    ... // onPopPage: (route, result) => route.didPop(result),

    pages 파라미터는 List<Page<dynamic>>를 사용하므로 LoginPage의 상위 객체인 단일 MaterialPage를 전달합니다.

    앱을 실행하면 LoginPage가 표시됩니다.

    FlutterApp-Module1Photo2-small

    사용자가 로그인하려면 먼저 등록할 수 있어야 합니다. 새 파일 sign_up_page.dart에 SignUpPage를 구현합니다.

    import 'package:flutter/material.dart';
    
    class SignUpPage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _SignUpPageState();
    }
    
    class _SignUpPageState extends State<SignUpPage> {
      final _usernameController = TextEditingController();
      final _emailController = TextEditingController();
      final _passwordController = TextEditingController();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SafeArea(
              minimum: EdgeInsets.symmetric(horizontal: 40),
              child: Stack(children: [
                // Sign Up Form
                _signUpForm(),
    
                // Login Button
                Container(
                  alignment: Alignment.bottomCenter,
                  child: FlatButton(
                      onPressed: () {},
                      child: Text('Already have an account? Login.')),
                )
              ])),
        );
      }
    
      Widget _signUpForm() {
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Username TextField
            TextField(
              controller: _usernameController,
              decoration:
                  InputDecoration(icon: Icon(Icons.person), labelText: 'Username'),
            ),
    
            // Email TextField
            TextField(
              controller: _emailController,
              decoration:
                  InputDecoration(icon: Icon(Icons.mail), labelText: 'Email'),
            ),
    
            // Password TextField
            TextField(
              controller: _passwordController,
              decoration: InputDecoration(
                  icon: Icon(Icons.lock_open), labelText: 'Password'),
              obscureText: true,
              keyboardType: TextInputType.visiblePassword,
            ),
    
            // Sign Up Button
            FlatButton(
                onPressed: _signUp,
                child: Text('Sign Up'),
                color: Theme.of(context).accentColor)
          ],
        );
      }
    
      void _signUp() {
        final username = _usernameController.text.trim();
        final email = _emailController.text.trim();
        final password = _passwordController.text.trim();
    
        print('username: $username');
        print('email: $email');
        print('password: $password');
      }
    }

    SignUpPage는 LoginPage와 거의 동일하지만 이메일을 입력하는 추가 필드가 있으며 버튼의 텍스트가 변경되었습니다.

    main.dart의 Navigator에도 SignUpPage를 MaterialPage로 추가하겠습니다.

    ... // home: Navigator(
    
    pages: [
      MaterialPage(child: LoginPage()),
      MaterialPage(child: SignUpPage())
    ],
    
    ... // onPopPage: (route, result) => route.didPop(result),

    이제 앱을 실행합니다.

    FlutterApp-Module1Photo3-small

    Navigator의 페이지 목록에서 구현된 마지막 페이지이므로, 이제 앱이 실행될 때 등록 화면이 표시됩니다. Navigator는 페이지 인수를 마지막 인수가 맨 위에 오는 스택처럼 처리합니다. 즉, 현재 LoginPage 위에 SignUpPage가 겹쳐 있는 형태로 표시됩니다.

    다른 페이지를 표시하려면 특정 페이지를 표시할 시기를 결정하는 로직을 목록 내부에 구현해야 합니다. 스트림을 생성하고 Navigator를 StreamBuilder에 중첩하여 이러한 업데이트를 수행할 수 있습니다.

    auth_service.dart라는 새 파일을 만들고 다음을 추가합니다.

    import 'dart:async';
    
    // 1
    enum AuthFlowStatus { login, signUp, verification, session }
    
    // 2
    class AuthState {
      final AuthFlowStatus authFlowStatus;
    
      AuthState({this.authFlowStatus});
    }
    
    // 3
    class AuthService {
      // 4
      final authStateController = StreamController<AuthState>();
    
      // 5
      void showSignUp() {
        final state = AuthState(authFlowStatus: AuthFlowStatus.signUp);
        authStateController.add(state);
      }
    
      // 6
      void showLogin() {
        final state = AuthState(authFlowStatus: AuthFlowStatus.login);
        authStateController.add(state);
      }
    }
    1. AuthFlowStatus는 로그인 페이지, 등록 페이지, 확인 페이지 또는 세션의 네 가지 인증 흐름을 포함할 수 있는 열거형입니다. 마지막 두 페이지를 곧 추가해 보겠습니다.
    2. AuthState는 스트림에서 관찰할 실제 객체이며 authFlowStatus를 속성으로 포함합니다.
    3. AuthService는 두 가지 목적으로 사용됩니다. 즉, AuthState의 스트림 컨트롤러를 관리하며, 다음 모듈에서 추가할 모든 인증 기능을 포함합니다.
    4. authStateController는 관찰할 새로운 AuthState의 다운스트림 전송을 담당합니다.
    5. 이는 AuthState 스트림을 signUp으로 업데이트하는 간단한 함수입니다.
    6. showSignUp과 동일한 기능을 수행하지만 스트림을 업데이트하여 로그인 정보를 전송합니다.

    main.dart를 다시 열고 _MyAppState에 AuthService 인스턴스를 추가합니다.

    ... // class _MyAppState extends State<MyApp> {
    
    final _authService = AuthService();
    
    ... // @override

    이제 Navigator를 StreamBuilder에 래핑할 수 있습니다.

    ... // theme: ThemeData(visualDensity: VisualDensity.adaptivePlatformDensity),
    
    // 1
    home: StreamBuilder<AuthState>(
        // 2
        stream: _authService.authStateController.stream,
        builder: (context, snapshot) {
          // 3
          if (snapshot.hasData) {
            return Navigator(
              pages: [
                // 4
                // Show Login Page
                if (snapshot.data.authFlowStatus == AuthFlowStatus.login)
                  MaterialPage(child: LoginPage()),
    
                // 5
                // Show Sign Up Page
                if (snapshot.data.authFlowStatus == AuthFlowStatus.signUp)
                  MaterialPage(child: SignUpPage())
              ],
              onPopPage: (route, result) => route.didPop(result),
            );
          } else {
            // 6
            return Container(
              alignment: Alignment.center,
              child: CircularProgressIndicator(),
            );
          }
        }),
        
    ... // MaterialApp closing ); 
    1. AuthState를 전송하는 스트림을 관찰할 StreamBuilder로 Navigator를 래핑했습니다.
    2. AuthService 인스턴스의 authStateController에서 AuthState 스트림에 액세스합니다.
    3. 스트림에 데이터가 있을 수도 있고 없을 수도 있습니다. AuthState 유형의 데이터에서 authFlowStatus에 안전하게 액세스하기 위해 여기에서는 먼저 검사를 구현합니다.
    4. 스트림이 AuthFlowStatus.login을 전송하면 LoginPage가 표시됩니다.
    5. 스트림이 AuthFlowStatus.signUp을 전송하면 SignUpPage가 표시됩니다.
    6. 스트림에 데이터가 없으면 CircularProgressIndicator가 표시됩니다.

    처음부터 스트림에 데이터가 있도록 하려면 값을 즉시 내보내야 합니다. _MyAppState가 초기화되면 AuthFlowStatus.login을 전송하여 이를 구현할 수 있습니다.

    ... // final _authService = AuthService();
    
    @override
    void initState() {
     super.initState();
     _authService.showLogin();
    }
    
    ... // @override

    지금 앱을 실행하면 스트림을 통해 내보낸 유일한 값인 LoginPage가 표시됩니다.

    이제 LoginPage와 SignUpPage 간을 전환할 수 있는 기능을 구현해야 합니다.

    login_page.dart로 이동하여 다음을 추가합니다.

    ... // class LoginPage extends StatefulWidget {
    
    final VoidCallback shouldShowSignUp;
    
    LoginPage({Key key, this.shouldShowSignUp}) : super(key: key);
    
    ... // @override

    이제 생성자가 main.dart에서 일부 기능을 트리거할 수 있으며 _LoginPageState에서 호출되는 인수로서 VoidCallback을 받습니다.

    등록 버튼에 대한 인수로 shouldShowSignUp을 _LoginPageState에 전달합니다.

    ... // child: FlatButton(
    
    onPressed: widget.shouldShowSignUp,
    
    ... // child: Text('Don\'t have an account? Sign up.')),

    다시 main.dart로 돌아가 LoginPage의 shouldShowSignUp 파라미터의 인수를 전달해야 합니다.

    ... // if (snapshot.data.authFlowStatus == AuthFlowStatus.login)
    
    MaterialPage(
       child: LoginPage(
           shouldShowSignUp: _authService.showSignUp))
    
    ... // Show Sign Up Page

    앱을 실행하고 LoginPage에서 등록 버튼을 누릅니다. SignUpPage로 이동됩니다.

    사용자가 화면 하단에 있는 버튼을 눌러 등록 페이지와 로그인 페이지 간을 전환할 수 있도록 SignUpPage에 대해서도 동일한 작업을 수행할 수 있어야 합니다.

    sign_up_page.dart에 다음을 추가합니다.

    ... // class SignUpPage extends StatefulWidget {
    
    final VoidCallback shouldShowLogin;
    
    SignUpPage({Key key, this.shouldShowLogin}) : super(key: key);
    
    ... // @override
    ... // child: FlatButton(
    
    onPressed: widget.shouldShowLogin,
    
    ... // child: Text('Already have an account? Login.')),

    LoginPage에 대해 구현한 것처럼, 사용자가 화면 하단에 있는 버튼을 누르면 SignUpPage가 VoidCallback을 트리거합니다.

    이제 shouldShowLogin의 인수를 받도록 main.dart를 업데이트합니다.

    ... // if (snapshot.data.authFlowStatus == AuthFlowStatus.signUp)
    
    MaterialPage(
       child: SignUpPage(
           shouldShowLogin: _authService.showLogin))
    
    ... // pages closing ],

    지금 앱을 실행하면 LoginPage와 SignUpPage 간에 전환할 수 있습니다.

    마지막으로 이들 각 페이지에는 로그인/등록을 위해 처리할 수 있는 자격 증명으로서 각 필드의 사용자 입력을 전달할 방법이 필요합니다.

    auth_credentials.dart라는 새 파일을 생성하고 다음을 추가합니다.

    // 1
    abstract class AuthCredentials {
      final String username;
      final String password;
    
      AuthCredentials({this.username, this.password});
    }
    
    // 2
    class LoginCredentials extends AuthCredentials {
      LoginCredentials({String username, String password})
          : super(username: username, password: password);
    }
    
    // 3
    class SignUpCredentials extends AuthCredentials {
      final String email;
    
      SignUpCredentials({String username, String password, this.email})
          : super(username: username, password: password);
    }
    1. AuthCredentials는 로그인 또는 등록을 수행하는 데 필요한 최소 정보의 기준선으로 사용할 추상화 클래스입니다. 따라서 LoginCredentials와 SignUpCredentials를 거의 상호 교환하여 사용할 수 있습니다.
    2. 로그인하는 데는 사용자 이름과 암호만 필요하므로 LoginCredentials는 AuthCredentials의 단순한 구체적 구현입니다.
    3. LoginCredentials와 거의 동일하지만 등록에 필요한 추가 필드로서 이메일이 추가됩니다.

    이제 AuthService에 로그인 및 등록 메서드를 추가할 수 있습니다. AuthService는 각 자격 증명을 받아 Navigator의 상태를 올바른 페이지로 변경합니다.

    다음 두 함수를 auth_service.dart에 추가합니다.

    ... // showLogin closing }
    
    // 1
    void loginWithCredentials(AuthCredentials credentials) {
     final state = AuthState(authFlowStatus: AuthFlowStatus.session);
     authStateController.add(state);
    }
    
    // 2
    void signUpWithCredentials(SignUpCredentials credentials) {
     final state = AuthState(authFlowStatus: AuthFlowStatus.verification);
     authStateController.add(state);
    }
    
    ... // AuthService closing }
    1. 사용자가 AuthCredentials를 전달하면 몇 가지 로직을 수행하고 최종적으로 사용자를 session 상태로 전환합니다.
    2. 등록하려면 확인 코드를 입력하여, 입력한 이메일을 확인해야 합니다. 따라서 등록 로직은 상태를 verification으로 전환해야 합니다.

    먼저 ValueChanged 속성을 통해 LoginCredentials를 전송하도록 LoginPage를 업데이트합니다.

    ... // class LoginPage extends StatefulWidget {
    
    final ValueChanged<LoginCredentials> didProvideCredentials;
    
    ... // final VoidCallback shouldShowSignUp;
    
    LoginPage({Key key, this.didProvideCredentials, this.shouldShowSignUp})
       : super(key: key);
    
    ... // @override

    이제 _LoginPageState의 _login() 메서드에서 자격 증명을 전달할 수 있습니다.

    ... // print('password: $password');
    
    final credentials =
      LoginCredentials(username: username, password: password);
    widget.didProvideCredentials(credentials);
    
    ... // _login closing }

    SignUpPage와 유사한 기능을 구현해 보겠습니다.

    ... // class SignUpPage extends StatefulWidget {
    
    final ValueChanged<SignUpCredentials> didProvideCredentials;
    
    ... // final VoidCallback shouldShowLogin;
    
    SignUpPage({Key key, this.didProvideCredentials, this.shouldShowLogin})
       : super(key: key);
    
    ... // @override

    자격 증명을 생성합니다.

    ... // print('password: $password');
    
    final credentials = SignUpCredentials(
       username: username, 
       email: email, 
       password: password
    );
    widget.didProvideCredentials(credentials);
    
    ... // _signUp closing }

    이제 main.dart의 모든 항목을 연결합니다.

    ... // child: LoginPage(
    
    didProvideCredentials: _authService.loginWithCredentials,
    
    ... // shouldShowSignUp: _authService.showSignUp)),
    ... // child: SignUpPage(
    
    didProvideCredentials: _authService.signUpWithCredentials,
    
    ... // shouldShowLogin: _authService.showLogin))

    이렇게 하면 LoginPage와 SignUpPage가 마무리되지만 AuthFlowStatus의 경우에서 보듯이 확인 페이지와 세션을 나타내는 페이지를 구현해야 합니다.

    새 파일 verification_page.dart에 VerificationPage를 추가합니다.

    import 'package:flutter/material.dart';
    
    class VerificationPage extends StatefulWidget {
      final ValueChanged<String> didProvideVerificationCode;
    
      VerificationPage({Key key, this.didProvideVerificationCode})
          : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _VerificationPageState();
    }
    
    class _VerificationPageState extends State<VerificationPage> {
      final _verificationCodeController = TextEditingController();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SafeArea(
            minimum: EdgeInsets.symmetric(horizontal: 40),
            child: _verificationForm(),
          ),
        );
      }
    
      Widget _verificationForm() {
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Verification Code TextField
            TextField(
              controller: _verificationCodeController,
              decoration: InputDecoration(
                  icon: Icon(Icons.confirmation_number),
                  labelText: 'Verification code'),
            ),
    
            // Verify Button
            FlatButton(
                onPressed: _verify,
                child: Text('Verify'),
                color: Theme.of(context).accentColor)
          ],
        );
      }
    
      void _verify() {
        final verificationCode = _verificationCodeController.text.trim();
        widget.didProvideVerificationCode(verificationCode);
      }
    }

    VerificationPage는 실제로 LoginPage의 축소된 버전일 뿐이며 위젯 트리의 상위로 확인 코드만 전달합니다.

    auth_service.dart로 돌아가 확인 코드를 처리하고 상태를 session으로 업데이트하는 메서드가 필요합니다.

    ... // signUpWithCredentials closing }
    
    void verifyCode(String verificationCode) {
     final state = AuthState(authFlowStatus: AuthFlowStatus.session);
     authStateController.add(state);
    }
    
    ... // AuthService closing }

    이제 main.dart의 Navigator에 VerificationPage를 추가합니다.

    ... // shouldShowLogin: _authService.showLogin)),
    
    // Show Verification Code Page
    if (snapshot.data.authFlowStatus == AuthFlowStatus.verification)
      MaterialPage(child: VerificationPage(
        didProvideVerificationCode: _authService.verifyCode))
    
    ... // pages closing ],
  • 카메라/갤러리 흐름 생성

    VerificationPage를 구현했으므로 이제 사용자가 로그인했을 때 표시할 UI를 구현할 수 있습니다. 이미지의 갤러리를 표시하고 디바이스 카메라로 사진을 찍는 기능을 만듭니다. 각 화면을 언제 표시할지 지시하는 상태 변경을 처리할 CameraFlow 위젯을 생성합니다.

    camera_flow.dart라는 새 파일을 만들고 다음을 추가합니다.

    import 'package:flutter/material.dart';
    
    class CameraFlow extends StatefulWidget {
      // 1
      final VoidCallback shouldLogOut;
    
      CameraFlow({Key key, this.shouldLogOut}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _CameraFlowState();
    }
    
    class _CameraFlowState extends State<CameraFlow> {
      // 2
      bool _shouldShowCamera = false;
    
      // 3
      List<MaterialPage> get _pages {
        return [
          // Show Gallery Page
          MaterialPage(child: Placeholder()),
    
          // Show Camera Page
          if (_shouldShowCamera) 
          MaterialPage(child: Placeholder())
        ];
      }
    
      @override
      Widget build(BuildContext context) {
        // 4
        return Navigator(
          pages: _pages,
          onPopPage: (route, result) => route.didPop(result),
        );
      }
    
      // 5
      void _toggleCameraOpen(bool isOpen) {
        setState(() {
          this._shouldShowCamera = isOpen;
        });
      }
    }
    1. CameraFlow는 사용자가 로그아웃할 때 상태 변경을 트리거하고 main.dart에서 상태를 다시 업데이트해야 합니다. GalleryPage를 생성하고 나서 잠시 후 이 기능을 구현하겠습니다.
    2. 이 플래그는 카메라가 표시되어야 하는지 여부를 결정하는 상태 역할을 합니다.
    3. _shouldShowCamera가 업데이트될 때 Navigator가 업데이트되도록 계산된 속성을 사용하여 현재 상태에 따라 올바른 탐색 스택을 반환합니다. 지금은 자리 표시자 페이지를 사용하고 있습니다.
    4. _MyAppState와 마찬가지로 Navigator 위젯을 사용하여 세션에 대해 지정된 시간에 표시할 페이지를 결정합니다.
    5. 이 메서드를 사용하면 호출 사이트에 setState()를 구현할 필요 없이 카메라 표시 여부를 전환할 수 있습니다.

    gallery_page.dart에 GalleryPage를 생성합니다.

    import 'package:flutter/material.dart';
    
    // 1
    class GalleryPage extends StatelessWidget {
      // 2
      final VoidCallback shouldLogOut;
      // 3
      final VoidCallback shouldShowCamera;
    
      GalleryPage({Key key, this.shouldLogOut, this.shouldShowCamera})
        : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Gallery'),
            actions: [
              // 4
              // Log Out Button
              Padding(
                padding: const EdgeInsets.all(8),
                child:
                    GestureDetector(child: Icon(Icons.logout), onTap: shouldLogOut),
              )
            ],
          ),
          // 5
          floatingActionButton: FloatingActionButton(
              child: Icon(Icons.camera_alt), onPressed: shouldShowCamera),
          body: Container(child: _galleryGrid()),
        );
      }
    
      Widget _galleryGrid() {
        // 6
        return GridView.builder(
            gridDelegate:
                SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
            itemCount: 3,
            itemBuilder: (context, index) {
              // 7
              return Placeholder();
            });
      }
    }
    1. GalleryPage는 단순히 이미지를 표시하므로 StatelessWidget으로 구현할 수 있습니다.
    2. 이 VoidCallback가 CameraFlow의 shouldLogOut 메서드에 연결합니다.
    3. 이 VoidCallback가 CameraFlow에서 _shouldShowCamera 플래그를 업데이트합니다.
    4. 로그아웃 버튼은 AppBar에서 작업으로 구현되며, 누르면 shouldLogOut을 호출합니다.
    5. 이 FloatingActionButton을 누르면 카메라가 표시되도록 트리거합니다.
    6. 2개의 열이 있는 그리드에 이미지가 표시됩니다. 여기서는 이 그리드에 3개의 항목을 하드코딩합니다.
    7. 스트로지 추가 모듈에서 이미지 로딩 위젯을 구현하겠습니다. 그때까지는 자리 표시자를 사용하여 이미지를 나타냅니다.

    이제 새로 생성한 CameraFlow._pages에서 자리 표시자를 GalleryPage로 바꿀 수 있습니다.

    ... // Show Gallery Page
    
    MaterialPage(
        child: GalleryPage(
            shouldLogOut: widget.shouldLogOut,
            shouldShowCamera: () => _toggleCameraOpen(true))),
    
    ... // Show Camera Page

    CameraPage 생성을 간소화하기 위해 pubspec.yaml 파일에 다음과 같은 몇 가지 종속 구성 요소를 추가합니다.

    ... # cupertino_icons: ^1.0.0
    
    camera:
    path_provider:
    path:
    
    ... # dev_dependencies:

    각 플랫폼의 몇 가지 구성도 업데이트해야 합니다.

    Android의 경우 minSdkVersion을 21로 업데이트합니다(android > app > build.gradle).

    ... // defaultConfig {
    ...
    
    minSdkVersion 21
    
    ... // targetSdkVersion 29

    iOS의 경우 Info.plist를 업데이트하여 카메라에 액세스할 수 있도록 합니다(ios > Runner > Info.plist).

    ... <!-- <false/> -->
    
    <key>NSCameraUsageDescription</key>
    <string>Need the camera to take pictures.</string>
    
    ... <!-- </dict> -->

    이제 camera_page.dart라는 새 파일에 다음을 추가합니다.

    import 'package:camera/camera.dart';
    import 'package:flutter/material.dart';
    import 'package:path/path.dart';
    import 'package:path_provider/path_provider.dart';
    
    class CameraPage extends StatefulWidget {
      // 1
      final CameraDescription camera;
      // 2
      final ValueChanged didProvideImagePath;
    
      CameraPage({Key key, this.camera, this.didProvideImagePath})
          : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _CameraPageState();
    }
    
    class _CameraPageState extends State<CameraPage> {
      CameraController _controller;
      Future<void> _initializeControllerFuture;
    
      @override
      void initState() {
        super.initState();
        // 3
        _controller = CameraController(widget.camera, ResolutionPreset.medium);
        _initializeControllerFuture = _controller.initialize();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: FutureBuilder<void>(
            future: _initializeControllerFuture,
            builder: (context, snapshot) {
              // 4
              if (snapshot.connectionState == ConnectionState.done) {
                return CameraPreview(this._controller);
              } else {
                return Center(child: CircularProgressIndicator());
              }
            },
          ),
          // 5
          floatingActionButton: FloatingActionButton(
              child: Icon(Icons.camera), onPressed: _takePicture),
        );
      }
    
      // 6
      void _takePicture() async {
        try {
          await _initializeControllerFuture;
    
          final tmpDirectory = await getTemporaryDirectory();
          final filePath = '${DateTime.now().millisecondsSinceEpoch}.png';
          final path = join(tmpDirectory.path, filePath);
    
          await _controller.takePicture(path);
    
          widget.didProvideImagePath(path);
        } catch (e) {
          print(e);
        }
      }
    
      // 7
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    }
    1. 사진을 찍으려면 CameraFlow에서 제공하는 CameraDescription 인스턴스를 받아야 합니다.
    2. 이 ValueChanged는 카메라에 캡처된 이미지의 로컬 경로를 CameraFlow에 제공합니다.
    3. CameraController 인스턴스를 준비하기 위해 initState 메소드에서 인스턴스를 초기화하고, 완료되면 _initializeControllerFuture를 초기화합니다.
    4. 그런 다음 FutureBuilder가 Future가 반환되는 시점을 관찰하고 카메라에 포착되는 미리 보기, 또는 CircularProgressIndicator를 표시합니다.
    5. FloatingActionButton은 _takePicture()를 트리거합니다.
    6. 이 메서드는 이미지 위치에 대한 임시 경로를 구성하고 didProvideImagePath를 통해 해당 경로를 다시 CameraFlow에 전달합니다.
    7. 마지막으로, 페이지가 삭제되고 나면 CameraController를 폐기해야 합니다.

    CameraFlow로 돌아가 CameraDescription 인스턴스를 생성해야 합니다.

    ... // class _CameraFlowState extends State<CameraFlow> {
    
    CameraDescription _camera;
    
    ... // bool _shouldShowCamera = false;

    _camera를 가져와 초기화하는 함수를 생성합니다.

    ... // _toggleCameraOpen closing }
    
    void _getCamera() async {
      final camerasList = await availableCameras();
      setState(() {
        final firstCamera = camerasList.first;
        this._camera = firstCamera;
      });
    }
    
    ... // _CameraFlowState closing }

    _CameraFlowState가 초기화되는 대로 즉시 이 함수를 호출합니다.

    ... // _pages closing }
    
    @override
    void initState() {
     super.initState();
     _getCamera();
    }
    
    ... // @override of build

    마지막으로, _pages의 자리 표시자를 CameraPage 인스턴스로 바꿉니다.

    ... // if (_shouldShowCamera)
    
    MaterialPage(
       child: CameraPage(
           camera: _camera,
           didProvideImagePath: (imagePath) {
             this._toggleCameraOpen(false);
           }))
    
    ... // _pages closing ];

    이제 CameraPage가 카메라로 초기화되고 이미지가 촬영되면 imagePath를 반환합니다. 여기서는 사진을 찍은 후 카메라를 닫기만 합니다.

  • 로그아웃 추가

    UI의 탐색 루프를 닫으려면 AuthService에 로그아웃 방식을 추가해야 합니다.

    ... // verifyCode closing }
    
    void logOut() {
     final state = AuthState(authFlowStatus: AuthFlowStatus.login);
     authStateController.add(state);
    }
    
    ... // AuthService closing }

    마지막으로, main.dart의 Navigator.pages에서 CameraFlow의 케이스를 구현합니다.

    ... // _authService.verifyCode)),
    
    // Show Camera Flow
    if (snapshot.data.authFlowStatus == AuthFlowStatus.session)
      MaterialPage(
          child: CameraFlow(shouldLogOut: _authService.logOut))
    
    ... // pages closing ],
  • 애플리케이션 테스트

    앱을 다시 실행하면 앱의 모든 화면을 탐색할 수 있습니다.

    EndofModule1-gif

결론

Flutter에서 Photo Gallery 앱의 UI를 구현했습니다. 프로젝트에 Amplify를 구현할 준비가 되었습니다.

이 모듈이 유용했습니까?

감사합니다.
좋아하는 사항을 알려주세요.
닫기
실망을 드려 죄송합니다.
오래되었거나 혼란스럽거나 부정확한 사항이 있습니까? 피드백을 제공하여 이 자습서를 개선할 수 있도록 도와주십시오.
닫기

Amplify 초기화