【Flutter】メモアプリでパスワードロック・生体認証ロック機能の実装に挑戦してみた！

![トップ2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228439/7356a55f-67ae-8929-5042-ca79983f4a38.jpeg)


7月にFlutter開発を始めてから２作目のメモアプリ「アイデアメモ iX」をリリースしました。
■AppStore
https://apps.apple.com/jp/app/id1517535550

■Google Play
https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja

■アプリの詳細記事
  https://yukio.site/idea_shuffle_memo/

その中でパスワードロックの機能を実装したのですが、今回はそのやり方を少しだけ紹介していきたいと思います。

■別の記事
【Flutter】GoogleDriveへのバックアップ・リストア機能を実装するまでの道のり
https://qiita.com/YuKiO-OO/items/67b471e6be6c4c4c26e9

【Flutter】メモアプリ開発で使ったオススメのパッケージを紹介
https://qiita.com/YuKiO-OO/items/283f44da64d304a6228e

【Flutter】もう怖くない！アプリ内課金・定期購入機能を実装する方法を丁寧に説明してみた。
https://qiita.com/YuKiO-OO/items/a0fe8e0a256afbb69fc7


# パスワードロック
<img width="375" alt="IMG_9928.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228439/eec04e67-724b-26be-979d-a09664b688b9.png">

#注意事項
リファクタリングもせず、試行錯誤した状態なので、コードが冗長的な所もありますのでご了承ください。

### 今回実装する機能
- メニューで、パスワードと生体認証によるロック画面の実装

※2020/6 時点の情報を元に作成をしています。

### 知っておいたほうがいい知識
- アプリのライフサイクル
パスワードロックでは、アプリのライフサイクルを知っておく必要があります。
ライフサイクルとは、簡単に言えばアプリの状態の流れのことです。アプリを閉じている状態、アプリを開いた状態など、アプリの状態には一連の流れがあるんです。
それでパスワードロック機能を実装するには、アプリが閉じた状態、開いた状態を感知して、適切な処理を走らせる必要があります。

ライフサイクルは、下記の記事がとても分かりやすく説明してくれているので、一読してみてください。素晴らしい記事をありがとうございます。
[Flutterでアプリの復帰やサスペンドを検出して処理を実行する](https://www.virment.com/catch-app-cycle-flutter/)

### 大まかな流れ

#### パスワードの設定
- パスワード設定のオンオフの記録
- パスワードの保存

#### パスワードロック画面の表示
- パスワード設定のオンオフをチェック。
- オンの場合、アプリが閉じた、もしくは開いた状態を感知して、パスワードロック画面を表示させる処理を走らせる。
オフの場合は、何もしない。
- パスワードロックもしくは生体認証を検証して合致したら、パスワードロック表示画面を閉じる


### 必要なパッケージ
#### passcode_screen
[passcode_screen | Flutter Package](https://pub.dev/packages/passcode_screen)
パスワードロック画面が簡単に作れるパッケージ

#### shared_preferences
[shared_preferences | Flutter Package](https://pub.dev/packages/shared_preferences)
データベースではなく、設定情報などが消えずに保存できるパッケージ

#### local_auth
[local_auth | Flutter Package](https://pub.dev/packages/local_auth)
生体認証を使うためのパッケージ。
生体認証がOKならTrueを返して、NGだったらFalseで返してくる。
難しそうに感じますが、結構シンプルな感じで、ありがたいパッケージです。

### 作るページ
- パスワードオン・オフ設定画面
- パスワード設定入力画面
- パスワード設定入力確認画面
- パスワードロック画面

作り方によっては、まとめられるかしれませんが、そっちのほうが分かりやすいので、バラバラに作ってます。

### この記事の前提
この記事では、基本的にstaful Widget内での実装なので、共通する部分は省いています。

```dart
class Example extends StatefulWidget {
  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {

    //この中に書いてあることが中心です。それ以外を書く場合には、分かりやすいようにその前後も記載するようにしています。
 
}
```

各パッケージなどはimportされているものとしています。
適宜、各パッケージのReadmeをチェックください。

わかりづらいところがあれば、コメントを気軽にしてください。
お答えできる部分があれば、お答えします。

### 構築の流れ
#### パスワードオン・オフ設定
この辺りは特に難しいことではありませんが、パスワードロックと生体認証のオンオフのスイッチを実装します。

```dart
 @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("パスワードセッティング"), 
      ),
      body: passwordSetWidget(),//パスワードのオンオフのスイッチを定義したメソットを呼び出し。
      bottomNavigationBar: Footer(selectedIndex: 4,),//フッターを共通化しているので呼び出し。
    );
  }



passwordSetWidget() {
  return SingleChildScrollView(
    child: Column(
      children: <Widget>[
        Row(
          children: <Widget>[//設定のタイトル
            Padding(
              padding: const EdgeInsets.all(12.0),
              child: Text("設定", style: TextStyle(fontSize: 14.0),),
            ),
          ],
        ),
        Card(//パスワードロックのスイッチ
          child: SwitchListTile(
            value: _password,
            onChanged: _setPassword,//オンになった時の処理
            activeTrackColor: Colors.blue,
            title: Row(
              children: <Widget>[
                Padding(
                  padding: const EdgeInsets.only(right: 10.0),
                  child: Icon(Icons.lock),
                ),
                Expanded(child: Text("パスワードロック")),
              ],
            ),
          ),
        ),
        Card(//生体認証のスイッチ
          child: SwitchListTile(
            value: _facePass,
            onChanged: _setFacePass,//オンになった時の処理
            activeTrackColor: Colors.blue,
            title: Row(
              children: <Widget>[
                Padding(
                  padding: const EdgeInsets.only(right: 10.0),
                  child: Icon(Icons.face),
                ),
                Expanded(child: Text("生体認証")),
              ],
            ),
          ),
        ),

      ],
    ),
  );
}



```

##### パスワードロックのスイッチ
パスワードロックがオンになったら、オンオフの状態を記録します。
最終的にパスワード1回目と２回目が合致したら、最終的にオンの状態を維持することになります。
この時、「パスワード忘れると、解除できなくなります。忘れないようにご注意ください！！」みたいな警告を出してあげたほうが優しいと思います。今回のサンプルではわかりづらくなるので、入れてません。

```dart
bool _password;

@override
  void initState() {
    // TODO: implement initState
  _getPasswordSetting();
  super.initState();

}

//パスワードのオンオフを記録するメソッド
//shared_preferencesパッケージの機能を使って、設定情報などを端末に保存します。
_isPasswordLock(bool value) async {
//受け取った引数true or falseをisPasswordLockという名前で保存してね！ってことにあります。
  var prefs = await SharedPreferences.getInstance();
  await prefs.setBool("isPasswordLock", value);
}

//パスワードのオンオフ設定を読み出すメソッド
_getPasswordSetting() async {
//これをinitstateで読み出しておいて、初期値としてセットしておきます。
  var prefs = await SharedPreferences.getInstance();
  _password = await prefs.getBool('isPasswordLock') ?? false;
//??falseを忘れると、何も設定されていない初期状態の時にエラーになります。
//nullだったら、falseを入れなさいってことになります。
  setState(() {
    _password;
  });
}



//パスワード設定のスイッチが変更された時の処理
_setPassword(bool value) async{

  await _isPasswordLock(value);//引数のValueをパスワードの設定状態として保存

  if(value == true) {
//このタイミングで一度ダイアログを表示したりして、パスワードを忘れると解除できませんのような文言を表示してもいいかもしれません。
          Navigator.push(
              context,
              MaterialPageRoute(
                  builder: (context) => PasswordSetting()));//パスワード設定画面の表示

  } else {
    await _isFacePass(false);//パスワードがオフになったら生体認証も強制的にオフ
    Navigator.pushReplacement(
        context,
        MaterialPageRoute(
            builder: (context) => PasswordSetScreen()));//スイッチを切り替えた状態にするために再読み込み
  }


}
```


##### 生体認証のスイッチ
生体認証がオンになったら、生体認証が起動。
生体認証がOKなら、オンにします。
ただし、パスワードロックが設定されていない場合には、オンにできない使用にします。
生体認証がうまくいかない場合に備えてです。
ちなみに公式ドキュメントで、かならずiOSとAndroidのネイティブ側の設定で、生体認証を使えるように設定しておきましょう。

生体認証はこの記事を参考にさせていただきました。
(https://qiita.com/coka__01/items/76af4ea73a6a8c8fa135)



#### IOSの設定でこれを追記
```Info.plist 
<key>NSFaceIDUsageDescription</key>
<string>Why is my app authenticating using face id?</string>
```

#### Android設定でこれを追記
```AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.app">//これすでにあるやつ
//これを追記するだけ。
  <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<manifest>//これすでにあるやつ
```

####　生体認証の処理

```dart
bool _facePass;

@override
  void initState() {
  _getPasswordSetting();//さっき追加してます。
  _getFacePassSetting();
  super.initState();
}

//生体認証のオンオフを保存するメソッド
_isFacePass(bool value) async { 
  var prefs = await SharedPreferences.getInstance();
  await prefs.setBool("isFacePass", value);
}

//生体認証のオンオフ設定を読み出すメソッド
_getFacePassSetting() async {
//パスワードと同じくinitstateで読み出しておきましょう。
  var prefs = await SharedPreferences.getInstance();
  _facePass = await prefs.getBool('isFacePass') ?? false;
//パスワードと同様に?? falseをしないと、初期状態でエラーになります。
  setState(() {
    _facePass;
  });
}

//生体認証のスイッチを変えた時の処理
_setFacePass(bool value) async{
  await _getPasswordSetting();

  if (_password == false && value == true ) {
//パスワード設定がオフで、生体認証がオンだった場合は、生体認証がオンにできないように警告を表示する設定をしておきましょう。
    return;
  }

  if (_password == true && value == true ) {
//パスワード設定がオンで、生体認証がオンになった場合
    var check = _authenticate(); //生体認証を発動

    if(check == true) { 
//生体認証が承認されたら
      await _isFacePass(value);//生体認証設定状態をオンとして保存
      return;//
    }
  }


  await _isFacePass(value);//生体認証をオフにされたら、オフの状態で保存


  Navigator.pushReplacement(
      context,
      MaterialPageRoute(
          builder: (context) => PasswordSetScreen())); //オフにするために画面を再読み込み。
}


//生体認証のタイプをチェック
Future<List<BiometricType>> _getAvailableBiometricTypes() async {
  List<BiometricType> availableBiometricTypes;
  try {
    availableBiometricTypes = await _localAuth.getAvailableBiometrics();
  } on PlatformException catch (e) {
    //エラーの処理
  }
  return availableBiometricTypes; //生体認証はこれやーと返す
}

//生体認証を呼び出して、結果を返す処理
Future<bool> _authenticate() async {
  bool result = false;
  List<BiometricType> availableBiometricTypes = await _getAvailableBiometricTypes(); //生体認証が指紋か顔かチェック

  try {
    if (availableBiometricTypes.contains(BiometricType.face)
        || availableBiometricTypes.contains(BiometricType.fingerprint)) {
      result = await _localAuth.authenticateWithBiometrics(localizedReason: "生体認証");
    }
  } on PlatformException catch (e) {
    //エラーの処理
  }
  return result;//承認した結果をtrue or falseで返す。
}
```
#### パスワードの設定画面
パスワードがオンになったら、まず最初にパスワードを入力してもらう画面を表示します。

```dart
  final StreamController<bool> _verificationNotifier =
  StreamController<bool>.broadcast();
  int passwordDigits = 4; //パスワードの桁数

  @override
  void dispose() {
    // TODO: implement dispose
    _verificationNotifier.close();
    super.dispose();
  }


  @override
  Widget build(BuildContext context) {
//パスワードロック画面を発動
    return _showLockScreen(
      context,
      opaque: false,/
    );
  }

//パスワードロック画面の見た目の詳細
//ボタンの大きさや、色などカスタマイズできます。
//各色の設定はmain.dartで定義しているカラーなどを設定しています。

  _showLockScreen(BuildContext context,
      {bool opaque,
        List<String> digits}) {

            return  PasscodeScreen(
                title: Column(
                  children: <Widget>[
                    Icon(Icons.lock, size: 30),
                    Text("パスワードを入力してください",
                      textAlign: TextAlign.center,
                      style: TextStyle(fontSize: 15),
                    ),
                  ],
                ),
                passwordDigits: passwordDigits,
                circleUIConfig: CircleUIConfig(
                  borderColor: Theme.of(context).dividerColor,
                  fillColor: Theme.of(context).dividerColor,
                  circleSize: 20,
                ),
                keyboardUIConfig: KeyboardUIConfig(
                  primaryColor: Theme.of(context).dividerColor,
                  digitTextStyle: TextStyle(fontSize: 25),
                  deleteButtonTextStyle: TextStyle(fontSize: 15),
                  digitSize: 75,
                ),
                passwordEnteredCallback: _onPasscodeEntered,//パスワードが入力された時の処理
                deleteButton: Icon(Icons.backspace, size: 15.0),
                cancelButton:  Icon(Icons.cancel, size: 15.0),
                cancelCallback:  _onPasscodeCancelled,//パスワードが入力がキャンセルされた時の処理
                shouldTriggerVerification: _verificationNotifier.stream,
                backgroundColor: Theme.of(context).primaryColor,
                digits: digits,
              );
  }

//パスワードが桁数まで入力された発動する処理
  _onPasscodeEntered(String enteredPasscode)  {

//パスワード確認フォームに飛ばす
    Navigator.pushReplacement(
        context,
        MaterialPageRoute(
            builder: (context) => ConfirmPassword(password: enteredPasscode))); //入力されているパスワードを引数として渡す。
  }

//入力がキャンセルされたら発動する処理
   _onPasscodeCancelled() async{
     await _isPasswordLock(false); //とりあえず、パスワード設定をオフにしておく
     Navigator.maybePop(context);
     Navigator.pushReplacement(
         context,
         MaterialPageRoute(
             builder: (context) => PasswordSetScreen())); //パスワード設定を再読み込み
  }

//パスワード設定のオンオフを記録する(shared_preferencesパッケージの機能です)
  _isPasswordLock(bool value) async {
    var prefs = await SharedPreferences.getInstance();
    await prefs.setBool("isPasswordLock", value);
  }
```



パスワードの入力が確認できたら、パスワード入力確認ページで飛ばします。
パスワードの確認をして、合致すればパスワードを保存します。
この時、キャンセルされば全ての設定を破棄します。

```dart
//クラスをまたぐので、クラスまで一応書いておきますね。

class ConfirmPassword extends StatefulWidget {
//これから処理するにあたって、パスワードの引数は必須ですよという設定をしておきます。だってパスワードない状態でこのページ開けてしまうのは困るので。

  String password = ""; 

  ConfirmPassword({@required this.password});//ぜってぇーパスワードを引数をとして送ってこいよなと言ってます。

  @override
  _ConfirmPasswordState createState() => _ConfirmPasswordState();
}

class _ConfirmPasswordState extends State<ConfirmPassword> {

  final StreamController<bool> _verificationNotifier =
  StreamController<bool>.broadcast();//入力状況を感知
  bool isAuthenticated = false;
  int passwordDigits = 4;  //パスワードの桁数
  String passLockMessage; //エラーメッセージ

//入力されたパスワードを保存する処理です。
  _setPassword(String value) async {
    var prefs = await SharedPreferences.getInstance();
    await prefs.setString("lockPassword", value);
  }

  @override
  void didChangeDependencies() {
    // TODO: implement didChangeDependencies
    super.didChangeDependencies();
   passLockMessage = "念のためパスワードを入力してください";
//機能実装都合でここに書いてありますが、initstate内に書いても通常は問題ないはずです。
  }

  @override
  void dispose() {
    // TODO: implement dispose
    _verificationNotifier.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return _showLockScreen(//パスワード設定画面をは発動します。
      context,
      opaque: false,
    );
  }

//再入力画面の設定
//ほぼほぼ前の方と同じです。
  _showLockScreen(BuildContext context,
      {bool opaque,
        List<String> digits}) {
      print(widget.password);
    return  PasscodeScreen(
      title: Column(
        children: <Widget>[
          Icon(Icons.lock, size: 30),
          Text(
           "パスワード",
            textAlign: TextAlign.center,
            style: TextStyle(fontSize: 15),
          ),
          Text(
            '$passLockMessage',//メッセージを表示します。
            textAlign: TextAlign.center,
            style: TextStyle(fontSize: 10),
          ),
        ],
      ),
      passwordDigits: passwordDigits,
      circleUIConfig: CircleUIConfig(
        borderColor: Theme.of(context).dividerColor,
        fillColor: Theme.of(context).dividerColor,
        circleSize: 20,
      ),
      keyboardUIConfig: KeyboardUIConfig(
        primaryColor: Theme.of(context).dividerColor,
        digitTextStyle: TextStyle(fontSize: 25),
        deleteButtonTextStyle: TextStyle(fontSize: 15),
        digitSize: 75,
      ),
      passwordEnteredCallback: _onPasscodeEntered,
      deleteButton: Icon(Icons.backspace, size: 15.0),
      cancelButton:  Icon(Icons.cancel, size: 15.0),
      cancelCallback:  _onPasscodeCancelled,
      shouldTriggerVerification: _verificationNotifier.stream,
      backgroundColor: Theme.of(context).primaryColor,
      digits: digits,
    );
  }

//パスワードが桁数入力されたときの処理
  _onPasscodeEntered(String enteredPasscode) async {

//パスワードのチェック
    bool isValid = widget.password == enteredPasscode;
    _verificationNotifier.add(isValid);//パスコードが正しいかどうかをパスコード画面に通知してます。
    if (isValid) {
      await _setPassword(enteredPasscode);//パスワードを保存
//忘れガチですが、パスワード設定のオンとオフ設定、スイッチをオンにした直後に設定しています。
      setState(() {
        passLockMessage = "";//メッセージを空に
        this.isAuthenticated = isValid; //trueにしています。
//
      });
    } else {
      setState(() {
        passLockMessage = "パスワードが一致しません";     //エラーメッセージを変数に格納

      });


    }
  }

//パスワードを再入力をキャンセルした場合
  _onPasscodeCancelled() async{
    await _isPasswordLock(false); //パスワードロック設定をオフ状態
    Navigator.maybePop(context);
    Navigator.pushReplacement(
        context,
        MaterialPageRoute(
            builder: (context) => PasswordSetScreen())); //パスワード設定画面に戻します。
  }

//パスワードロック設定のオンオフを記録
  _isPasswordLock(bool value) async {
    var prefs = await SharedPreferences.getInstance();
    await prefs.setBool("isPasswordLock", value);
  }


}
```






#### どこで処理を走らせるか
パスワードは設定できましたが、一つ考えるべきところが、どこでパスワード を表示される処理を走らせるか問題です。
後ほど、スマホの状態を感知して、処理を走らせるわけですが、すべてのページにその実装をするのは辛い。

今回作ったアプリでは、共通ページとして、フッター専用のページを作って、それぞれのページで参照しています。

```dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("タイトル"), 

      ),
      body: passwordSetWidget(),
      bottomNavigationBar: Footer(),
  }
```

このフッター部分の状態を見て、パスワードロックの処理を走らせます。


#### パスワード機能を
##### スマホの状態で処理を発動
状態の管理として、４つに分類されます。今回はpuouseの時に処理を走らせるようにします。
というのも、それ以外の処理では、他の機能でページを読み込まれた時にも、パスワードロック画面が発動してしまうからです。

```dart
//状態を管理するには、Classにwith WidgetsBindingObserverが必要。
class _Footer extends State <Footer> with WidgetsBindingObserver{
 bool _isPasswordLock = false;

//パスワード設定のオンオフを読み出し
  _getPasswordSetting() async {
    var prefs = await SharedPreferences.getInstance();
    _isPasswordLock = await prefs.getBool('isPasswordLock');
    setState(()  {
    });
  }

  void initState() {
    // TODO: implement initState
    super.initState();
    WidgetsBinding.instance.addObserver(this);//スマホの状態を感知する
    WidgetsBinding.instance.addPostFrameCallback((_) async{
      await _getPasswordSetting();
    });//widgetの構築が終わったら、発動させる処理。
}

  @override
  void dispose() {
    // TODO: implement dispose
    WidgetsBinding.instance.removeObserver(this);//スマホの状態を感知終了
    super.dispose();
  }

//スマホ状態によって処理を走らせます。
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // TODO: implement didChangeAppLifecycleState
//    super.didChangeAppLifecycleState(state);

    if (state == AppLifecycleState.inactive) {
//アプリの画面が非アクティブな時。
    } else if (state == AppLifecycleState.paused) {
      _passWordLock();//アプリが完全にバックグランドになったら、パスワードロック画面を表示を発動。ただし表示されるのは、画面が再び立ち上がったあと。

    } else if (state == AppLifecycleState.resumed) {
//アプリが復帰した時の処理。これでもいいが、他の処理で何度も呼び出されることがあり、変なところでパスワードロックがかかってしまった。
    }
  }
}

```



#### パスワードロックを表示
スマホのアプリがバックグランドになったら、パスワードロックを表示させます。

```dart
//状態を管理するには、Classにwith WidgetsBindingObserverが必要。２重でパスワードロック画面が表示されないように、ここでも状態管理をしています。

class _PassWordScreenState extends State<PassWordScreen>
    with WidgetsBindingObserver {
  final StreamController<bool> _verificationNotifier =
      StreamController<bool>.broadcast();//入力状態をチェック
  bool isAuthenticated = false; //パスワード入力が正しいか
  String passLockMessage = "";//エラーメッセージ
  int passwordDigits = 4;//入力する桁数
  bool passwordView = false;//パスワード画面が表示されているか？
  bool _facePass = false; //FacePassの設定
  String _password = ""; //パスワード
  LocalAuthentication _localAuth = LocalAuthentication(); //生体認証のインスタンス

//パスワードの読み出し
  _getPassword() async {
    var prefs = await SharedPreferences.getInstance();
    _password = await prefs.getString("lockPassword");
  }
//生体認証の読み出し
  _getFacePassSetting() async {
    var prefs = await SharedPreferences.getInstance();
    _facePass =  prefs.getBool('isFacePass');
    setState(() {

    });
  }

//生体認証の種類の確認
  Future<List<BiometricType>> _getAvailableBiometricTypes() async {
    List<BiometricType> availableBiometricTypes;
    try {
      availableBiometricTypes = await _localAuth.getAvailableBiometrics();
    } on PlatformException catch (e) {
        // エラーを入力
    }
    return availableBiometricTypes;
  }

//生体認証をチェックしてOKだったらパスワードを処理を飛ばす。
  Future<bool> _authenticate() async {
    bool result = false;
    List<BiometricType> availableBiometricTypes = await _getAvailableBiometricTypes();
    try {
      if (availableBiometricTypes.contains(BiometricType.face)
          || availableBiometricTypes.contains(BiometricType.fingerprint)) {
        result = await _localAuth.authenticateWithBiometrics(localizedReason: 生体認証);
      }
    } on PlatformException catch (e) {
      // エラーを入力
    }
    if (result == true) {
        return  _onPasscodeEntered(_password);//生体認証がOKだったら、ロックを解除するために、パスワード付きで解除するメソッドを発動。
    }
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    WidgetsBinding.instance.addObserver(this);//スマホの状態をチェック
    _getPassword(); //パスワードを読み出し
    _getFacePassSetting();　//生体認証の読み出し
  }

  @override
  void didChangeDependencies()  async {
    super.didChangeDependencies();
//生体認証を少しだけ遅らせます。そうしないと、パスワードロック画面が表示しきる前に発動してしまい、パスワードロック画面の表示が中途半端になってしまいます。
    await Future.delayed(Duration(milliseconds: 700));
    if(_facePass != false) {
      await _authenticate();
    }
  }

  @override
  void dispose() {
    // TODO: implement dispose
    _verificationNotifier.close();//入力状態確認の終了
    WidgetsBinding.instance.removeObserver(this);  WidgetsBinding.instance.addObserver(this);//スマホの状態確認の終了
    super.dispose();
  }

//スマホの状態確認
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // TODO: implement didChangeAppLifecycleState
 super.didChangeAppLifecycleState(state);

    if (state == AppLifecycleState.inactive) {

    } else if (state == AppLifecycleState.paused) {
//パスワードロック画面が表示されているので、この状態でスマホ閉じて開くと、パスワードロック画面が２重の状態になります。なのでパスワードロック画面が開いている状態では、画面を入れ替えるようにします。
      Navigator.pushReplacement(
          context,
          MaterialPageRoute(
              builder: (context) => PassWordScreen()));

    } else if (state == AppLifecycleState.resumed) {

    }
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
        child:  _showLockScreen(
        context,
        opaque: false,
    ),
//アンドロイドの場合、これを入れないと、戻るボタンでパスワードロック画面が閉じてしまいます。
        onWillPop:() async => false);

  }


//パスワード設定画面とほぼ一緒

  _showLockScreen(BuildContext context,
      {bool opaque,
        List<String> digits})  {

    return  PasscodeScreen(
      title: Column(
        children: <Widget>[
          Icon(Icons.lock, size: 30),
          Text(
            'Enter App Passcode', 
            textAlign: TextAlign.center,
            style: TextStyle(fontSize: 15),
          ),
          Text(
            '$passLockMessage', 
            textAlign: TextAlign.center,
            style: TextStyle(fontSize: 10),
          ),
        ],
      ),
      passwordDigits: passwordDigits,
      circleUIConfig: CircleUIConfig(
        borderColor: Theme.of(context).dividerColor,
        fillColor: Theme.of(context).dividerColor,
        circleSize: 20,
      ),
      keyboardUIConfig: KeyboardUIConfig(
        primaryColor: Theme.of(context).dividerColor,
        digitTextStyle: TextStyle(fontSize: 25),
        deleteButtonTextStyle: TextStyle(fontSize: 15),
        digitSize: 75,
      ),
      passwordEnteredCallback: _onPasscodeEntered,
      deleteButton: Icon(Icons.backspace, size: 15.0),
      shouldTriggerVerification: _verificationNotifier.stream,
      backgroundColor: Theme.of(context).primaryColor,
      digits: digits,
    );
  }

  _onPasscodeEntered(String enteredPasscode) {
    bool isValid = _password == enteredPasscode;
    _verificationNotifier.add(isValid);
    if (isValid) {
      setState(() {
        passLockMessage = "";
        this.isAuthenticated = isValid;//パスワードが合致した状態にんすr。
      });
      Navigator.pop(context);
    } else {
      setState(() {
        passLockMessage = "パスワードが違います。再入力してください。";
      });
    }
  }
}

```


### ハマりポイント
解決できなかった部分が、パスワードロック画面の表示のタイミングです。
本来であれば、アプリを閉じて、スマホを立ち上げた段階で、パスワードロック画面が表示されている状態が好ましいです。
しかし、Flutterの仕組み上、画面推移の処理が走るのが、画面が立ち上がったあとなので、少しだけ画面が見えてします。

ネイティブ側で処理をすれば、解決できるそうですが、ネイティブはさわれないので、今回は、問題なしと判断して、この部分は詰めませんでした。


### まとめ
試行錯誤して作成したため、まだコードなどがうまくリファクタリングできていません。かなり冗長的なコードになっているので、時間を見つけて処理していきたいと思います。
完成の状態を見たい方は、ぜひアプリをダウンロードしてみてください。

あと、四苦八苦して作ったアプリも是非ともよろしくお願いします。アプリのアイデア出しに使ってみてください。

■AppStore
https://apps.apple.com/jp/app/id1517535550

■Google Play
https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja

■アプリの詳細記事
https://yukio.site/idea_shuffle_memo/


あとツイッターもやってますので、ぜひチェックください。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Webサービスやアプリ！<br><br>斬新なアイデアをひらめきたい人に！<br><br>アイデアを発想するためのメモアプリ✍️<br><br>IdeaShuffleMemoをリリースしました！<br><br>■AppStore<a href="https://t.co/gEPzEEJ7mt">https://t.co/gEPzEEJ7mt</a><br><br>■Google Play<a href="https://t.co/w0vTiOanGE">https://t.co/w0vTiOanGE</a><br><br>使ってみてください😁<a href="https://twitter.com/hashtag/%E9%A7%86%E3%81%91%E5%87%BA%E3%81%97%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%A8%E7%B9%8B%E3%81%8C%E3%82%8A%E3%81%9F%E3%81%84?src=hash&amp;ref_src=twsrc%5Etfw">#駆け出しエンジニアと繋がりたい</a> <a href="https://twitter.com/hashtag/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0?src=hash&amp;ref_src=twsrc%5Etfw">#プログラミング</a> <a href="https://t.co/gQ0dOMNgGB">pic.twitter.com/gQ0dOMNgGB</a></p>&mdash; YuKiO | 個人開発&amp;Flutter (@oo_forward) <a href="https://twitter.com/oo_forward/status/1278611399749431296?ref_src=twsrc%5Etfw">July 2, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
それは、また。
次は、GoogleDriveのバックアップ処理について書いていきたいと思います。