FlutterでbottomNavigationBarを実装する

ネイティブアプリでよく見る、画面下部にあるナビゲーションバーのことを、ボトムナビゲーションバーというそうです!
ナビゲーションバーだとアンドロイドで言うと「◀」「●」「■」が並ぶエリアと呼び方がかぶってしまうので、「ボトムナビゲーションバー」が呼び名なのだとか。

ScaffoldでbottomNavigationBarを指定します。

class _MyHomePageState extends State<MyHomePage> {
  int _currentIndex = 0; // currentIndexにデフォルト値を与えないとコンパイルエラーになる

  @override
  Widget build(BuildContext context){
    return Scaffold(
      appBar: AppBar(
        title: Text('Title'),
      ),
      body: Center(
        child: Text('bottomNavigationBar test')
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        items: [
          new BottomNavigationBarItem(
            icon: new Icon(Icons.home),
            title: Text("Home")
          ),
          new BottomNavigationBarItem(
            icon: new Icon(Icons.settings),
            title: Text("Settings")
          ),
        ],
        onTap: (int index) {
          print(index); // デバッグ用にprintしてみる
        },
      ),
    );
  }
}

こんな感じで、実装はシンプル。
特徴的なのは、onTapがBottonNavigationBarItem(アイコン)側ではなくて、BottomNavigationBar側で準備されていること。
onTapは引数としてintを持っているので、このintを元に画面遷移を実装するようです!

Navigatorクラスを使って実装していくようですが、続きはまた明日やっていきます!

Gitでユーザ名とかパスワードを求められた時

新しいパソコンでGitの設定を間違ったり忘れたりしていて、少し戸惑ったのでメモ。

  • git pullでPermission denied
$ git pull
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

SSH keyの登録を忘れていました😇
GitHubでssh接続する手順~公開鍵・秘密鍵の生成から~ - Qiitaを参考に公開鍵・秘密鍵のセットを作って登録します。

  • git pushでユーザ名とパスワードを聞かれる

remote urlでhttpsを使用していました😇
sshを使って接続します。

git remote -v
origin  https://github.com/makicamel/test.git (fetch)
origin  https://github.com/makicamel/test.git (push)

GitHubリポジトリの「Clone or download」ボタンを押した時に「Clone with SSH」と表示されていたら「Use SSH」をクリック、表示されたgit:github.comから始まるアドレスをコピー。

git remote set-url origin git@github.com:makicamel/test.git

git remote -v
origin  git@github.com:makicamel/test.git (fetch)
origin  git@github.com:makicamel/test.git (push)

秘密鍵パスフレーズを設定していました😇
ssh-agentに秘密鍵を登録します。

ssh-add ~/.ssh/id_rsa
  • 参考リンク

ssh-agentを利用して、安全にSSH認証を行う - Qiita

それでは快適なGitライフを!

Androidの仮装端末からインターネットに接続できない時の対処法

今日は(も)小ネタ。

Androidアプリの開発中、たまによくある(らしい)Wifiがつながらない事案。

そんな時はapp/src/main/AndroidManifest.xmlに以下を記述します。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.hoge_app">
<uses-permission android:name="android.permission.INTERNET" /> <!-- Added -->

アプリのインターネットへの接続を行うことができるようになる設定と教えていただきました!

ただ今回の原因はアプリではなく仮装端末自体がWifiにつながっておらず、
理由は不明で仮装端末の再起動で直りました😅

Androidの実機の際はWifiは端末(実機)依存なのだそうですが、
仮装端末だと起こりがちな事象だというお話を伺いました!

flutter clean & エミュレータの再起動を習慣にしようと改めて思った事案でした!

間違ってremoteにpushしたファイルを削除したい時

誤ってパスワードなどの秘密情報が含まれたファイルをremoteにpushしてしまった時。
普通にgit rm hogeしただけだと当然ながら履歴が残ってしまうので、履歴ごと改変したい時のやり方です。
リモートの履歴を改変することになるので、チーム開発の際は混乱が起きない様ご注意ください。

前提条件

こんな履歴があったとします。

$ git log --oneline
fae133a (HEAD -> master, origin/master) 3rd commit.
3ee314f 2nd commit.
a3af1e4 Initial commit.

よく見ると、パスワードが書かれたsecret.fileをcommit(&push)しています!

$ git log -p -2
commit fae133a633e21e7feb94c3994c276ef86d41210c (HEAD -> master, origin/master)
Author: makicamel
Date:   Tue Mar 19 19:00:30 2019 +0900
    3rd commit.
# (略)

commit 3ee314fc2f06bb7118229dc25ece3b54af889fb4
Author: makicamel
Date:   Tue Mar 19 19:00:00 2019 +0900

    2nd commit.

diff --git a/secret.file b/secret.file
index e69de29..9c3f6c8 100644
--- a/secret.file
+++ b/secret.file
@@ -0,0 +1 @@
+PASSWORD # <= これを消したい!

git rm secret.fileをするとファイルは消せますが、履歴は消せません。
履歴自体を消したいので、commitからやり直します。

commitを取り消し

まずはローカルで変更を修正。
git logで戻りたい履歴のコミットのハッシュ値を確認、そこまで戻ります。
git resetオプションはこちらに詳しいですが--hard--mixed--softとあります。

$ git log --oneline
fae133a (HEAD -> master, origin/master) 3rd commit.
3ee314f 2nd commit.     # <= ここでsecret.fileを追加した
a3af1e4 Initial commit. # <= ここに戻れればOK

今回はファイルの変更はそのまま、コミットだけ取り消したいので--softします。

$ git reset a3af --soft
$ git log --oneline
a3af1e4 (HEAD -> master) Initial commit.

ファイルをgit管理対象から外す

.gitignoreに外したいファイルを追記。

$ echo "secret.file" > .gitignore

ただしこのままpushしても、gitが管理してくれているままになるので、git rmします。
git rmだとファイルごと削除されるので、ファイルを残すよう--cachedオプションをつけて実行。

$ git rm --cached secret.file

remoteにpush

あとはremoteにpushするだけ!
リモートとローカルで不整合が起きるので、-fオプションでpushします。

$ git push -f

無事履歴からファイルを削除することができました!

入門!アトミックデザインに行ってきました

今日は@nrsさんのAtomicDesign入門に行ってきました!
振り返りと理解の確認のためにまとめを書かせて頂きます。

登壇資料はこちら。

speakerdeck.com

AtomicDesignとは

モジュール分割を基本としたデザインシステム(デザインの指針・デザインの共通言語)のこと。
モジュールは粒度によって下記の5つのステージに分けられる。

  • Atoms 原子: 最小のコンポーネント。ex. Logo, TextBox
  • Molecules 分子: ふたつ以上のAtomsを組み合わせたコンポーネント
    ex. HeaderLink(Logo + HeaderTitle), SearchBox(TextBox + PrimaryButton)
  • Organisms 生物: AtomsとMoleculesを組み合わせたコンポーネント
    ex. Header(HeaderLink + SearchBox)
  • Templates ページの基礎となるコンテンツ構造、ワイヤーフレームのこと。
    ex.Header + BreadCrampList + Contents
  • Pages テンプレートにデータを流し込んだもの

※ Templatesから「原子どこ行った??」状態なのは、Atom〜Organismsまではエンジニア/デザイナー向け、Templates以上は上流のお歴々と話をする為の道具となるからじゃないでしょうか、とのこと。
Atomとは、「UIとしての機能性を破壊しない最小要素」。
もしButtonは矩形とラベルにわけると、ラベルは押せないのでButtonとしての機能を失う
だから、Buttonが最小要素(Atom)。

なるほどわかりやすい!

けどやっぱり、そうシンプルな話でもない。
AtomicDesignは5階層に分けることでモジュールの乱立を防ぐことができることが重要なのだけど、いつでもAtom -> Molecules -> Organismsと分けられるわけではない。

たとえば、こんなページがあったとすると

Atoms: TableCell, PagingButton
Molecules: TableRow(TableCellの集まり), PagingMenu(PagingButtonの集まり)

Atoms: TabButton, TabContent
Molecules: TabMenu(TabButtonの集まり)

Organismはこうなる

Organisms: Table(TableRow + PagingMenu), Tab(TabMenu + TabContent)

でも、Organism同士であるTableとTabを足しても、これはTemplateとは言えない。
(HeaderとかFooterとかSideMenuとか足りない)
…おや?🤔

AtomicDesignは使えないのか?

わけではない。
AtomicDesignはデザインシステムである。

AtomicDesignの目的

  • ❌AtomicDesignに沿って作ること
  • ⭕デザイナーとエンジニアの協業のための共通基盤づくり
    -> 共通認識のもとで会話ができるようになること!

コンポーネント指向

すべてはコンポーネントであるという考え方

Webだと、image, textbox, buttonetc, ぜんぶ組み合わせで作れてしまう。
ゲームやネイティブアプリだと、全てコンポーネントを作る(必要がある)。

AtomicDesignとは粒度にルールを加えたコンポーネント指向のこと。

たとえばよくあるこんなケース

デザイナー💁「モーダルに『閉じる』ボタンをつけたい!」
コンポーネント化していないと
エンジニア「いくつあるのかわからないモーダル、全部修正してテストするのか😇」
コンポーネント化していると
エンジニア「Modalコンポーネントにつければいけるぜ😋」

AtomicDesignのメリット: 変更に強くなる

AtomicDesign実践入門

どこからつくる?🤔

  1. サンプルページを作る
    デザイナー💁「こんな感じにしよっか」
  2. Atomの切り出しの検討
  3. 組み合わせてMoleculesを作る
  4. 組みあわせてOrganismsを作る
    2〜4の工程をデザイナーとエンジニアを一緒にやれたらベスト!!
    (おすすめはホワイトボードだけど、Web上でやったりとかでもOK)
  5. 4まで共通認識が出揃ったら各デザイナー、エンジニアが実装
    ※ プロパティやイベントを持った最小要素(Atoms)を仮実装->Molecules->Organismsと進める
    (下位要素があれば上位要素もそれで作れるから)
    ※ 同じレイヤーの作業は別の人と重複することがあるので、作業分担は最初に要素を洗い出すことが大事
  6. Template
    Templateのコード化は必須ではないが、もしするなら全体のTemplateとコンテンツのTemplateで分けるのがおすすめ
  7. Organismsの粒度
    さっきの例のように、Atom -> Molecules -> Organismの三段階では足りなくなるパターンがある。
    → OrganismはOrganismをコンポジションする
    つまり、Organism + Organism = Organismもアリ

AtomicDesignは5階層であることに意味があるけれど、忠実にすることが難しさにつながるならその本質を見極めて取捨選択するべき

AtomicDesignは守ることを目的とした不変の法律ではなく、開発に関わる全ての人達の指標
目的はデザイナーとエンジニアの協業のための共通基盤づくり
指標があるとお互いがお互いをリスペクトして仕事しやすくなる。

まとめ

  • AtomicDesignとは
    粒度にルールを加えたコンポーネント指向のこと
  • AtomicDesignのメリット
    変更に強くなる、保守性があがること
  • AtomicDesignの目的
    デザイナーとエンジニアが共通認識のもとで会話・協業ができるようになること

デザイナー💁「これに従ってデザインするね」
エンジニア😍「やりやすい!」

こんな会話が生まれるためのデザインシステム、それがAtomicDesign。


AtomicDesignとは、を学ばせて頂くと同時に、終始「よりよくするには?」を根底に感じる1時間でした。
ありがとうございました!!

参考: 原典AtomicDesign

Flutterでgoogleにログインする

毎日Flutter。
今日はFirebaseで認証をします。

Pubはこれ(firebase_auth)ですね。

google_sign_inを追加する

まずはgoogle_sign_inを追加します。
pubspec.yamlに下記追記して、flutter packages get

dependencies:
  flutter:
    sdk: flutter
  image_picker:
  google_sign_in: ^4.0.1+1 # new!
  firebase_storage:

main.dartimportを追記します。

import 'package:google_sign_in/google_sign_in.dart';

firebase_authを追加する

build.gradleclasspathを追記するのは前回やったので省略。
pubspec.yamlmain.dartにそれぞれ下記追記します。pubspec変更後はflutter packages get

dependencies:
  flutter:
    sdk: flutter
  image_picker:
  google_sign_in: ^4.0.1+1
  firebase_auth: # new!
  firebase_storage:
import 'package:firebase_auth/firebase_auth.dart';

ログイン画面の追加

main.dartに下記を追記し、MyAppクラスからMyGoogleLoginPage()を呼び出すように変更します。

class MyGoogleLoginPage extends StatefulWidget {
  @override
  _MyGoogleLoginPageState createState() => _MyGoogleLoginPageState();
}

class _MyGoogleLoginPageState extends State<MyGoogleLoginPage>{
  final GoogleSignIn _googleSignIn = GoogleSignIn();
  final FirebaseAuth _auth = FirebaseAuth.instance;

  Future<FirebaseUser> _handleSignIn() async {
    final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
    final GoogleSignInAuthentication googleAuth = await googleUser.authentication;

    final AuthCredential credential = GoogleAuthProvider.getCredential(
      accessToken: googleAuth.accessToken,
      idToken: googleAuth.idToken,
    );

    final FirebaseUser user = await _auth.signInWithCredential(credential);
    print("signed in " + user.displayName);
    return user;
  }

  @override
  Widget build(BuildContext context){
    return Scaffold(
      appBar: AppBar(
        title: Text('none'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            SizedBox(height: 24.0),
            FlatButton(
              child: Text('SignIn'),
              onPressed: () => _handleSignIn()
                .then((FirebaseUser user) => print(user))
                .catchError((e) => print(e))
            ),
          ],
        ),
      ),
    );
  }
}

googleにログインができるようになりました!

FlutterでFirebaseのStorageを使う〜後編〜

昨日(FlutterでFirebaseのStorageを使う〜前編〜)の続きです。

AndroidXに対応する

さて、昨日、Firebaseを使うようbuild.gradleに設定を加えたところ、大量のエラーが出てビルドができなくなりました😨
エラーメッセージ中に「ここ見てね!」とリンクが記述されているので、素直に見に行きます。
ざっくりいうと、android.supportというライブラリが非推奨になって、AndroidXってライブラリに変更されたよ!とのこと。

migrateの方法なども記載されているのですが、自動でAndroidX化してくれる方法を載せてくれている人がいたのでその方法で対応します。

# android/gradle.propertiesに下記を追加
android.useAndroidX = true
android.enableJetifier = true

無事ビルドが通るようになりました!

firebase_storageをインポート

Pubでそれっぽいものを探すと、firebase_storageが出てきます。これだ!
image_pickerと違ってExampleは書かれていないので、手探りでやります💪

まずはpubspec.yamlfirebase_storageを追加。

dependencies:
  flutter:
    sdk: flutter
  image_picker:
  firebase_storage: # new!

main.dartでインポートします。

import 'package:firebase_storage/firebase_storage.dart';

Flutter製チャットアプリを支える技術を参考にuploadImageメソッドを書きます。
(格好悪いけど、直接getImageから呼び出す)

class _MyHomePageState extends State<MyHomePage> {
  Future getImage() async {
    // (略)
    uploadImage(image);
  }
  Future<String> uploadImage(File file) async {
    int timestamp = DateTime.now().millisecondsSinceEpoch;
    String subDirectoryName = 'images';
    final StorageReference ref = FirebaseStorage()
        .ref()
        .child(subDirectoryName)
        .child('${timestamp}');
    final StorageUploadTask uploadTask = ref.putFile(
        file,
        StorageMetadata(
          contentType: "image/jpeg",
        ));
    StorageTaskSnapshot snapshot = await uploadTask.onComplete;
    if (snapshot.error == null) {
      return await snapshot.ref.getDownloadURL();
    } else {
      return 'Something goes wrong';
    }
  }
  // (略)
}

※ 実行の際にMissingPluginException(No implementation found for ...)というエラーがでる場合は、flutter clean & flutter packages get & flutter runでうまくいく場合があります。

さて、実行すると、firebase-auth is not linkedno auth token for requestといったメッセージが表示されますが、画像のアップロードに関してのエラーはないみたい!

わくわくしながらFirebaseのStorageを見てみると、タイムスタンプの名前のついた画像がアップロードされました!🎉