AWS の開始方法

Flutter アプリケーションを構築する

AWS Amplify を使用してシンプルな Flutter アプリケーションを作成する

モジュール 1: Flutter アプリを作成してデプロイする

このモジュールでは、Flutter アプリケーションを作成し、AWS Amplify のウェブホスティングサービスを使ってそれをクラウドにデプロイします。

はじめに

AWS Amplify は、より迅速なアプリ開発を実現する一連のツールです。ユーザーの認証、ファイルの保存、分析イベントのキャプチャ、その他多数の機能を数行のコードだけで実装できる使いやすいライブラリを提供します。

このモジュールでは、フォトギャラリーアプリの UI を構築します。これにはサインアップフロー、画像を表示するギャラリーのページ、カメラで写真を撮影する機能が含まれます。

このモジュールではアプリの基礎を築いて、その後のモジュールでは具体的なカテゴリに関する実際の Amplify 実装に焦点を当てます。

学習内容

  • サインアップおよびログインフローの実装
  • 画面間のナビゲーション
  • ウィジェットのグリッドの実装
  • デバイスのカメラを使用した写真撮影

主要な概念

Navigator - このチュートリアルでは、Flutter Navigator 2.0 を使用します。これは宣言型の実装を使って、表示する画面を決めるためのページリストを使用します。

コールバック - データをあるオブジェクトから別のオブジェクトへ送るため、通信にコールバックを使用します。コールバックは、呼び出しサイトから引数を渡されるという点で関数と似ていますが、コードは別の場所で実行します。

 所要時間

30 分

 使用するサービス

実装

  • Visual Studio Code を起動し、新しい Flutter プロジェクトをお好きな名前で作成します。

    プロジェクトがセットアップされたら、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 を返します。Scaffold により、ウィジェットをモバイルデバイスに合わせてフォーマットすることができます。
    3. このアプリは複数のデバイスで実行が可能であるため、SafeArea を確認することが重要です。ここでは、ログインフォームが画面の縁から縁までに広がらないよう、画面の左右両側にパディングを加える最小の EdgeInsets も活用します。
    4. この UI は、主となるログインフォームと、ユーザーがログインの代わりにサインアップを行うための画面下部のボタンとで構成されます。ここでは、子ウィジェットそれぞれの配置操作を容易にするためにスタックを使用します。
    5. _loginForm 関数の作成は完全に任意ですが、作成しておくと、build メソッドを若干すっきりさせることができます。ここでは、ユーザー名およびパスワードのテキストフィールドとログインボタンの UI を実装します。
    6. サインアップボタンには対話型の文を使用します。これで、アカウントを持っていないユーザーはサインアップすることができます。onPressed 機能はまだ実装されていません。
    7. _login メソッドは、TextField コントローラーからの値の抽出と、AuthCredentials オブジェクトの作成を行います。ここでは、各コントローラーの値の表示のみを行います。

    LoginPage の UI はまだ完成していません。main.dart の Navigator に追加します。

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

    ページのパラメータは List<Page<dynamic>> という形式をとります。そこで、この LoginPage が子となっている単一の MaterialPage を渡します。

    アプリを実行すると LoginPage が表示されるはずです。

    ユーザーは、サインインの前にサインアップする必要があります。新しいファイル 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 とほぼ同じですが、E メール用のフィールドが追加され、ボタンのテキストが変更されている点が異なります。

    ここでも、SignUpPage を MaterialPage として main.dart の Navigator に追加します。

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

    アプリを実行します。

    Navigator のページリストの最後に実装されているため、アプリ起動時にはこのサインアップ画面が表示されるはずです。Navigator はページの引数をスタックのように後入れ先出し方式で処理します。つまり、現在表示されている SignUpPage は、LoginPage の上に重ねられたものです。

    別のページを表示したいときは、リスト内にロジックを実装して、特定のページをいつ表示するかを決める必要があります。そのためには、Stream を作成し、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 は、認証フローが含まれる 4 つの状態 (ログインページ、サインアップページ、検証ページ、セッション) をカバーしたリストです。最後の 2 つのページをこれから追加します。
    2. AuthState は、ストリームで監視することになる実際のオブジェクトであり、そこにはプロパティとして authFlowStatus が含まれます。
    3. AuthService には 2 つの目的があります。AuthState のストリームコントローラーを管理すること、そして次のモジュールで追加する認証機能のすべてを含めることです。
    4. authStateController は、監視対象である新しい AuthState のダウンストリームを送信します。
    5. これは、AuthState ストリームを signUp に更新するシンプルな関数です。
    6. 行うことは showSignUp と同じですが、ストリームを更新することでログインを送信します。

    再度 main.dart を開いて、_MyAppState に AuthService のインスタンスを作成します。

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

    これで、StreamBuilder で Navigator をラップできます。

    ... // 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. AuthState ストリームには、AuthService のインスタンスの authStateController からアクセスします。
    3. このストリームにはデータが含まれる場合と含まれない場合があります。データから authFlowStatus に安全にアクセスするには、AuthState タイプの場合、まずここにチェックを実装します。
    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

    コンストラクターが VoidCallback を引数として受け入れます。VoidCallback は、main.dart で機能をトリガーしたり、_LoginPageState から呼び出したりすることができます。

    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 をトリガーします。

    次に、main.dart を更新して shouldShowLogin の引数を受け入れます。

    ... // 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 とほぼ同じですが、サインアップの場合は、E メールフィールドの追加が必要です。

    これで、ログインおよびサインアップのメソッドを AuthService に追加できます。AuthService は、それぞれの認証情報を受け入れ、Navigator の状態を適切なページに変更します。

    これら 2 つの関数を 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. サインアップでは、入力された E メールアドレスを、検証コードの入力により検証することが必要です。そのため、サインアップのロジックにより、状態が verification に変更される必要があります。

    まず、LoginPage を更新して ValueChanged プロパティから LoginCredentials を送信します。

    ... // 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 }

    次に、VerificationPage を main.dart の Navigator に追加します。

    ... // 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 が更新されるようにするために、computed プロパティを使って、現在の状態に基づく適切なナビゲーションスタックを返します。ここでは、Placeholder ページを使用します。
    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. 画像の読み込みウィジェットは、「ストレージを追加する」モジュールで実装します。それまでは、画像は Placeholder で表します。

    これで、新たに作成した GalleryPage を使って CameraFlow._pages の Placeholder を置き換えることができます。

    ... // 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 の Placeholder を 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 ],
  • アプリを再度実行すると、アプリのすべての画面に移動できるはずです。

まとめ

Flutter でフォトギャラリーアプリの UI を正しく実装できました。 プロジェクトに Amplify を実装する準備ができました。

このモジュールは役に立ちましたか?

Amplify を初期化する