AWS 入門

建置 Flutter 應用程式

使用 AWS Amplify 建立簡單的 Flutter 應用程式

第一單元︰建立和部署 Flutter 應用程式

在本單元中,您將建立一個 Flutter 應用程式,並使用 AWS Amplify 的 Web 託管服務將其部署到雲端。

簡介

AWS Amplify 是一套工具,透過提供易於使用的程式庫,開發人員只需使用幾行程式碼即可輕鬆驗證使用者,存放檔案,擷取分析事件等,從而更快地建置應用程式。

在本單元中,您將建立和建置 Photo Gallery 應用程式的使用者界面。這包括註冊流程,用於檢視影像的資料庫頁面,以及使用相機拍照的功能。

本單元將為我們的應用程式奠定基礎,因此以下單元可專注於特定類別的實際 Amplify 實作。

您將學到的內容

  • 實作註冊和登入流程
  • 在螢幕間導覽
  • 實作小工具網格
  • 使用裝置相機拍照

主要概念

導覽器 - 本教學將使用 Flutter Navigator 2.0,其透過頁面清單來確定應使用聲明性實作顯示的檢視。

回撥 - 為了將資料從一個物件傳送至另一個,我們將使用回撥進行通訊。回撥與函數類似,可以從叫用站點傳遞參數,但是在其他位置執行程式碼。

 完成時間

30 分鐘

 使用的服務

實作

  • 建立 Flutter 專案

    啟動 Visual Studio 程式碼,並使用您選擇的名稱建立一個新的 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 的首頁小工具是導覽器,其允許以聲明性方式設定導覽。
  • 建立身份驗證流程
    在將頁面新增至導覽器之前,我們需要建立代表每個頁面的小工具。我們從登入頁面開始,將其放入新檔案 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. 我們的使用者界面由主要登入表單和螢幕底部的按鈕組成,允許使用者註冊而非登入。我們在這裡使用堆疊,使操作每個子小工具的放置更加容易。
    5. 建立 _loginForm 函數完全可選,但確實會使建置​​方法有些混亂。在這裡,我們為使用者名稱和密碼文字欄位,以及登入按鈕實作了 UI。
    6. 退出註冊按鈕將採用互動式句子的形式,允許使用者在沒有帳戶的情況下註冊。尚未實作 onPressed 功能。
    7. _login 方法將負責從文字欄位控制擷取值,以及建立 AuthCredentials 物件。現在只需輸出每個控制器的值。

    LoginPage 的 UI 尚未完成,我們將其新增至 main.dart 的導覽器中。

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

    頁面參數採用 List<Page<dynamic>>,因此我們傳入單一 MaterialPage,其中 LoginPage 為子級。

    執行該應用程式,您應看到 LoginPage。

    FlutterApp-Module1Photo2-small

    使用者需要先註冊才能登入。我們在新檔案中實作 SignUpPage sign_up_page.dart

    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 的導覽器中將 SignUpPage 新增為 MaterialPage。

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

    現在執行該應用程式。

    FlutterApp-Module1Photo3-small

    現在,應用程式啟動時應會顯示註冊螢幕,因為它是導覽器頁面清單中實作的最後一個頁面。導覽器將頁面參數視為堆疊,其中最後一個位於頂部。這意味著我們目前看到 SignUpPage 堆疊在 LoginPage 的頂部。

    如果要顯示不同的頁面,必須在清單內部實作邏輯,以確定何時顯示特定頁面。我們可以建立串流,並將導覽器嵌套於 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

    現在,我們可以將導覽器封裝在 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. 我們在導覽器中封裝了 StreamBuilder,預期會觀察到發出 AuthState 的串流。
    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

    我們的建構函數現在接受 VoidCallback 作為參數,其可觸發 main.dart 中的某些功能,並從 _LoginPageState 叫用。

    在我們的 _LoginPageState 中,將 shouldShowSignUp 作為註冊按鈕的參數:

    ... // 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 完全相同,但電子郵件是註冊的必填欄位。

    現在,我們可以向 AuthService 新增登入和註冊方法,這會接受相應的登入資料,並將導覽器的狀態變更為正確的頁面。

    將這兩個函數新增至 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 時,我們將執行一些邏輯,並最終將使用者置於工作階段狀態。
    2. 註冊後,需要輸入驗證碼來驗證輸入的電子郵件。因此,註冊邏輯應變更狀態以進行驗證。

    我們首先來更新 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,需要一種方法來處理驗證碼並將狀態更新為工作階段。

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

    現在,將 VerificationPage 新增至 main.dart 的導覽器。

    ... // shouldShowLogin: _authService.showLogin)),
    
    // Show Verification Code Page
    if (snapshot.data.authFlowStatus == AuthFlowStatus.verification)
      MaterialPage(child: VerificationPage(
        didProvideVerificationCode: _authService.verifyCode))
    
    ... // pages closing ],
  • 建立相機/庫流程

    實作 VerificationPage 後,我們可以在使用者登入時移至使用者界面。我們將顯示影像庫,並且能夠使用裝置相機拍照。我們將建立 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 更新時更新我們的導覽器,我們使用了運算屬性來根據目前狀態返回正確的導覽堆疊。我們目前正在使用預留位置頁面。
    4. 與 _MyAppState 類似,我們使用導覽器小工具來確定應在工作階段的任何給定時間顯示哪個頁面。
    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. 我們的影像將顯示在包含兩欄的網格中。我們目前正在對該網格中的 3 個項目進行硬編碼。
    7. 我們將在「新增儲存體」單元中實作影像載入小工具。在此之前,我們將使用預留位置來表示影像。

    現在,我們可以用新建立的 GalleryPage 取代 CameraFlow._pages 中的預留位置。

    ... // 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. 為了拍照,我們需要取得 CameraDescription 的執行個體,該執行個體由 CameraFlow 提供。
    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

    最後,用 CameraPage 執行個體取代 _pages 中的預留位置

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

    現在使用相機初始化 CameraPage,並在拍攝影像後返回 imagePath。我們目前僅在拍照後才關閉相機。

  • 新增登出

    若要關閉使用者界面的導覽循環,我們需要向 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