ボクダイモリ

Life is like a Game

2019年の振り返りと来年の抱負

今年の振り返り

今年も振り返りと来年の抱負を書いていきます。 去年にブログ書いたときから状況が変わっているのでその点踏まえながら今年やってきた事を書いていきます。

私生活の振り返り

今年の9月辺りに引っ越しをしました。 今まで住んでたところが借りてから5年以上経つので色々ガタがきていたのが大きな理由です。この頃はフリーランスで何ヶ月か仕事をこなしていた頃でお金に余裕があったのでちょっといい物件に引っ越しました。引っ越し前の物件と比べると高所で日当たりが良く、建物も新しいところでめちゃめちゃ住みやすいです。 特に湿気が篭りにくいというのがかなり生活に効いてて部屋は汚れにくくて掃除が楽になりました。

仕事の振り返り

去年の末頃はWeb系の企業でエンジニアしてました。仕事内容は既存Webアプリーションの運用保守だったり機能追加だったり。新規のWebアプリケーション開発も担当していて、インフラの設計からAPI設計、DB設計、実装、テストなど幅広くやらせてもらってました。 ただ色々と訳があり、今年の1月には退職しました。その後はフリーランスエンジニアとして設計、実装メインで今まで活動しています。 担当する主な技術領域はバックエンドですが、プロジェクトの状況次第でインフラの構築や運用保守だったりReact+Typescriptで構成されたフロントエンドの開発を担当したりもしていました。 現在はスタートアップ企業からの案件でゼロからWebアプリケーションの開発に携わっています。

仕事の振り返りとしては全体的に充実した年になったと思います。 初めてのフリーランスで案件に携わり、自分の深めたい技術であるGo言語を仕事で使う事ができたり、フロントエンドの開発に携わる事で自分の持つ技術領域を広げる事ができました。 特にフロントエンドに対しては最初は苦手意識があったものの、慣れていく頃には自分でWebアプリケーションを作ってみたりなど自分にとって関心の高い技術の1つとなりました。 また、フリーランスになった際に収入がかなり増えました。おかげでちょっと家賃の高い所に引っ越すこともできて、自分の趣味や関心のある学問に投資をすることもできました。 自由な時間も増えました。 その他にもリモート稼働が可能な案件に携わっていて、柔軟に勤務時間を変えることもできるので結構自由に働けてますし自分の時間も増えました。 この一年は今までの社会人生活のでも大きく働き方を変えた一年でしたが、そのおかげで今とても充実してるのでチャレンジして良かったと思います。

趣味の振り返り

・テニス 今でもこの趣味は続いてて週1,2くらいでやってます。去年よりも確実に上達しているのを感じるのでめちゃ楽しいです。今までの人生の中でこんなに長く続く趣味は無かったので本当に好きなんだと思います。

・バイオリン これは去年の9月頃から始めてましたが今年の2月からやらなくなりました。 理由はバイオリンに対する優先度が低くなったのと引っ越しによってそれまで通っていた教室に行きづらくなったことです。 バイオリンは近くに教室と練習できそうなスタジオがないのでモチベーションが上がらない感じです。ただ、音楽を学んでいきたいという思いはあるのでもしかしたら何かのきっかけにまたやり始めるかもしれません。

・英語 これは以前からあの手この手で学習を継続しようとしては失敗してきました。 英語を学ぶ目的ですが1つは海外の情報をインプットするための技術を得るためです。エンジニアは調べ物をする際、英語の文献を当たることが多くあります(利用するツールやライブラリは海外製が多く、日本語のドキュメントがなかったりする)。エンジニアとして英語の情報を正しく収集できることはとても大事なことなので、英語の記事やドキュメントを理解できるだけの英語力を身につけたいと思ってます。 2つ目の理由は、海外で自由に行動できるようになるためです。将来的には海外でも自由にいきたい所に行って働けるようなりたいと思ってます。 英語を学びたい理由はあるのですが残念ながら今まで計画的に学習を進められたことはありませんでした。 ですが現在は毎日speakbuddyというアプリを使って学習を続けられています。使ってて楽しいので今回は続きそうな気がします。 https://www.speakbuddy.me/ 英語学習には今後も引き続き注力していこうと思います。

コーディング

今年は去年よりもGithubに草を生やすことができた。 今年 f:id:anrakusan:20191231201915p:plain 去年 f:id:anrakusan:20191231201922p:plain 今年はフロントエンド技術を色々試してみたりgolangでツールを書いたりしていたからかだいぶcontributions数が伸びてる。 9~10月あたりちょっと白いですがこの時期は引っ越しでごたごたしてた時かな。

来年の抱負

来年は今後長い間エンジニアとしてやっていくための能力を身につけるためにいくつか目標を立てました。下記が来年の目標です。 ・Atcoderで緑帯になる ・体系的なCSの知識を身につける ・英語の記事を難なく読めるようになり、海外旅行で困らないだけの会話技術を習得する ・機械学習の知識を身につけて仕事にする ・AWSサービスの構築を通して基礎的な知識を身につける

学習の優先度的にはアルゴリズム、英語をが高め、それ以外はこれらよりも低く設定します。 学習時間は1.5時間以上は取る想定。 また、それぞれの目標に対しては進展度を可視化し、毎日進捗を確認する。 学習する時間が無いときは無理せず優先度の高い部分だけ学ぶ、もしくはその日は休みます。

おわりに

今年は正直あまり勉強とかアウトプットとかできてなかった気がしますが、来年の具体的にやりたいことや目標が決まったので今年よりも色々できるようになるんじゃないかとワクワクしてます。 以上、それでは良いお年を!

FirebaseとReact+Typescriptでチャットアプリを作ってみた

最近仕事でフロントエンドの開発をする機会があったので、勉強がてらチャットができるWebアプリを作ってみました。
実装の際にこだわった点としてクラスコンポーネントは使用せずに関数コンポーネントだけで実装することを徹底しました。
また、なるべくany型を使わずボタンのイベントを受け取る部分やライブラリが値を返す部分などもなるべく型を書くようにしてます。

github.com

Firebaseにデプロイしたのがこちら

https://react-chat-6e910.firebaseapp.com/

アプリのUIはスマホに対応していないのでスマホで見ると結構残念な感じになります。PCで見てください。 この記事では実装のポイントとかを書いていきます。

認証が必要なコンポーネントへのルーティング

アプリではログインせずにチャット画面に行こうとするとログイン画面にリダイレクトされます。 これを実現するため、ルーティングに使用するRouteコンポーネントにログインチェック処理をラップしたPrivateRouteコンポーネントを実装しました。 ルーティングはsrc/App.tsxに定義しています。

const App: React.FC = () => {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/" component={Login} />
        <Route path="/login" component={Login} />
        <PrivateRoute path="/chat/:roomID" component={Chat} />
        <Route exact path="/rooms" component={RoomList} />
        <Route path="/rooms/new" component={NewRoom} />
      </Switch>
    </BrowserRouter>
  );
}

PrivateRouteの実装は同じ階層のPrivateRoute.tsxにあります。 loginUserが存在すると受けとったコンポーネントに、存在しなければ/loginに遷移します。

const PrivateRouteComponent: FC<State> = ({
  loginUser,
  component: Component,
  ...rest
}) => {
  return (
    <Route
      {...rest}
      render={props =>
        loginUser !== null ? (
          <Route component={Component} {...props} />
        ) : (
          <Redirect to="/login" />
        )
      }
    />
  );
}

画面遷移を行わず動的に画面を変更する

画面遷移を行う方法としてaタグを使ったりformのPOST処理時に画面遷移をさせるということができますが、
アプリではいちいち画面遷移させたくなかったので、これらの方法は使用しませんでした。 代わりにreact-router-domパッケージにあるLinkタグが使えますが、これはJSXを書けるところでしか使えません。 そこで任意の箇所でHTMLを再構築できるようにreact-router-domのwithRouterを使います。 withRouterでラップされたコンポーネントはpropsからhistory APIにアクセスできるようになるので、これを操作して画面遷移を実現します。
例としてsrc/containers/newRoom.tsxではルーム作成ボタンを押された時に実行される関数内で、/rooms/newに遷移するようになっています。

const RoomListComponent = withRouter((props: RoomListProps) => {
  const { rooms } = props;
  const CreateNewRoom = (
    e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
  ) => {
    e.preventDefault();
    props.history.push(`/rooms/new`);
  };

レスポンシブ対応

CSSの設定にはstyled-componentsを使用していて、中でmediaクエリを設定しているだけです。
CSSは苦手というか見た目で頑張る気が起きなかったので一部の画面だけレスポンシブになってます。

const RoomList = styled.div`
  display: inline-flex;
  flex-wrap: wrap;
  width: 100%;
  padding-right: 1%;

  div {
    /* モバイルファースト */
    margin-top: 1%;
    margin-left: 1%;
    flex-basis: 100%;

    @media screen and (min-width: 768px) and (max-width: 1024px) {
      flex-basis: 49%;
    }

    @media screen and (min-width: 1024px) {
      flex-basis: 24%;
    }
  }
`;

チャット画面から抜ける際、メッセージのデータを初期化する

stateにあるメッセージを初期化する際、React HooksのuseEffectという機能を使っています。
下記はsrc/containers/Chat.tsxでの一例です。

const Chat = withRouter((props: ChatProps) => {
  const { match, initialDispatch } = props;
  useEffect(() => {
    return () => initialDispatch([]); // 以前訪れたチャットルームのメッセージが表示されないように初期化する
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

useEffect内ではコンポーネントのライフサイクルに応じて実行する処理を定義することができます。 上記ではinitialDispatch関数を実行する関数をreturnしていますが、これはコンポーネントがアンマウントされるタイミングで実行されます。 ReactのクラスコンポーネントでいうとcomponentWillUnmount()と同様の動きになります。
なぜこのようなことをしているかというとコメントに書いてある通りですが、以前訪れたチャットルームのメッセージをstateに残したままだと 別のチャットルームに入った時、一瞬だけ前に入ったチャットルームのメッセージが表示されてしまうためです。

止むを得ずany型を使用した箇所

ここで書くのは実装のポイントというか実装できなかった事です。 any型がある箇所は例えばsrc/containers/LoginOAuth.tsxです。

export interface DispatchProps {
  dispatchAuthStatus: (user: firebase.User | null) => void;
  handleGoogleLogin: () => void;
  handleTwitterLogin: () => void;
  handleGuestLogin: (e: any) => void; // なんとも忌々しいany型
}

any型のeではformからsubmitされた値が入ります。受け取るのはゲストログインのフォームからなのでユーザ名がsubmitのイベントに入ります。 handleGuestLoginの処理は下記のように定義しています。

  handleGuestLogin: e => {
    e.preventDefault();
    const loginName = e.currentTarget.children.loginName.value;
    const user: User = { displayName: loginName, photoURL: '' };
    dispatch(changeAuthStatus(user));
  },

フォームに入力されたユーザ名はe.currentTarget.children.loginName.valueで取得できます。 このsubmitされるloginNameをうまく型解決することができなかったためany型を使用しています。
そもそも入力フォームのname部分を型として制御できるのかとかその型を管理する意義があるかは置いといて、 うまく型解決する方法があるなら知りたいですね。

最後に

React書くの楽しいので今後も何か書くと思います。
CSSは疲れたのでもう頑張りたくないです。黙ってmaterial-uiとか使います。
あとフォームのバリデーションとかセキュリティ対策みたいなそういうことは何もしてないので、ちょっとドキドキしながらアプリを公開しています😌

2018年の振り返り、来年の抱負

今日で2018年も終わりですね。今年の出来事を思い返してみると長かったような短かったような、仕事もプライベートも程々に挑戦していたと思いますが全体的にはのんびり生きていたような気がします。 一番大きなイベントと言えば転職で 去年の11月に新卒で入社した会社を辞めて今年1月からWeb系の会社に就職しました。もうそろそろ1年経ちましたが自分のやりたいと思ってたことはだいたい出来ていて社風も合っていたのでなんとか社会人としてやれています。今はなんとか社会人っぽく生きてはいますが最近は深夜まで何かに集中していた方が幸福度が高まるということを実感してからどんどん生活がだらしなくなっていくのを感じています。実に幸せです。反省はしていない。

今回は今年の振り返りについて書きますが特に書きたいこととかあまりないのでさらっと書きます。

Golang

業務で書く機会もあったしプライベートでも好きで書いてたり。 書いたコードは適当にgithubに上げている。

https://github.com/anraku/TodoList

https://github.com/anraku/tsk

https://github.com/anraku/chat

Goは読みやすいし書き方もシンプルになるのがいいですね。
上にClean Architectureで実装したものがあるのでいずれ学んだこととか解説とか書くかもしれない。

趣味

バイオリン

最近バイオリンを趣味で始めた。今までほとんど楽器や音楽に触れてこなかったので楽器何かやりたいなーとなんとなく思ってたので。 なぜバイオリンなのかというとただ単に弾くまでの準備が簡単そうでいつでも弾けそうだったから。自分はメンドくさがりなのでセッティングが煩雑だったりするとまず続かないと思っている。バイオリンは調弦したらすぐ弾けると思ってた。 あとは自宅の近くにバイオリンの教室があったため。一人で楽器を始めるのは難易度高いと思ってたのでありがたかった。 バイオリンの音は好きだけどそこまでバイオリンに思い入れはない。もし自宅の近くのウクレレの先生がいたらたぶんウクレレ弾いてたと思う。 実際バイオリンをやってみると思った通り準備は簡単だった。ただ意外と音が大きいので騒音に気をつける必要がある。
バイオリン初めてもう5ヶ月くらいになるけども毎週先生の元で練習したり小さな発表会に参加したりなんだかんだで続いている。いつ飽きるか分からないけどもこれからも続けていく。

ゲーム

スプラトゥーンとスマブラやってます。楽しい!

読んだ本

今年もいくつか技術書を読んだので良かった本を取りあげてみる。

  • エンジニアの知的生産術

Amazon CAPTCHA

内容としてはKJ法などの知的生産のためのツールを使うための具体的な方法だけでなく、人の記憶の仕組みなどに基づいて効率的な記憶の仕方などに渡ります。学び方のコツなどを知りたいという方にはオススメ。 題名は「エンジニアの」とついているけどもエンジニアではない人も十分に役に立つと思われる。

  • エンジニアリング組織論への招待

Amazon CAPTCHA

組織論、マネジメントについての本で「エンジニアリング」することについて個人やチーム、企業というそれぞれの単位で考察されている。 エンジニアリングにおける課題を解決するための思考法やメンタリング手法、プロジェクト管理手法などを学ぶことができる。 内容としては技術書というよりはマネジメントの本だと思われるが組織に属する方であれば読むと参考になることが多いはず。

  • Clean Architecture

Amazon CAPTCHA

ソフトウェア設計について分野全体を俯瞰しながらSOLID原則やデザインパターン、オブジェクト思考について学ぶことができる。 自分はクリーンアーキテクチャについて学びたくてこの本を取ったが、上記の事も合わせて広く学べたので読んで良かった。

おわりに

このブログ見返してみるとあんまり振り返れてない気がする。まぁいいか。 来年の抱負は彼女を作ることです。

それでは来年もよろしくお願いいたします。よいお年を。

【Rails5】ユーザパスワード更新の実装パターンまとめ

こんばんは

最近業務でRailsを使った社内用のアプリケーション開発を行なっていたのでそれについて何か書いてみます。

アプリケーションにユーザ管理機能を実装する際は、deviseというrubyのgemを使うことで簡単にユーザ管理に必要な機能を実装できます。 ユーザ情報への入力パラメータに対してバリデーションなどを簡単に実現できますが、独自に実装を加えたりする必要も場合によってはあります。 今回はdeviseを使ったユーザ管理を実装していく中で学んだこと、特にパスワードの更新周りで調べた点をまとめてみます。

rubyと各gemのバージョン

  • ruby 2.5.1p57
  • rails (5.2.1.1)
  • devise (4.5.0)

パスワード更新のパターン

  • ユーザ更新時にパスワードを更新しない
  • ユーザ更新時に特定の条件でパスワードの検証をしない
  • ユーザ更新時にパスワードフォームの値が空でない場合にパスワードを更新する

ユーザ更新時にパスワードを更新しない

ユーザ更新時にパスワードを更新したくない場合、strong parameterに:passwordを含めない方法が簡単です。

params(:user).permit(:first_name, :last_name, :address)

上記は独自のコントローラー内でupdateを行う場合になりますが、Devise::RegistrationsControllerを実装したコントローラーに実装する場合は下記のようになります。 update_resourceをoverrideして中でupdate_resourceをresource.update_without_passwordを呼ぶように実装を変更します。

# app/controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController

  protected

  def update_resource(resource, params)
    resource.update_without_password(params)
  end
end

ユーザ更新時に特定の条件でパスワードの検証をしない

deviseのvalidatableを実装することでupdate時にパスワードの検証を行うことができます。

class User < ApplicationRecord
    devise :validatable
end

validatableのパスワード検証のロジックはpassword_required内で定義されています。

      def password_required?
        !persisted? || !password.nil? || !password_confirmation.nil?
      end

このメソッドをoverrideすることで任意の条件でパスワード検証を行うことができます。

      def password_required?
          if do_validate # パスワード検証を行う条件
              !persisted? || !password.nil? || !password_confirmation.nil?
          else
              false
          end
      end

ユーザ更新時にパスワードの値が空だった場合、パスワードを更新しない

これは同僚から聞いて知ったのですが、controller内で参照できるparamsに含まれるパラメータは全てvalidationの対象となるようです。 なのでupdateメソッド内にパスワードが空だった場合、userをupdateする前にparams[:user].delete(:password)を実行することで validationを回避することができます。

  def update
    if params[:user][:password].blank?
      params[:user].delete("password")
    end
    if @user.update(user_params)
  end

おわりに

それぞれ簡単にですがdevise利用を踏まえてのパスワード更新周りについてまとめました。 deviseは便利な機能が多いので使う場合はどんな機能があるのかをざっと確認しておくと良いです。 機能の全体像を把握する上で下記の記事が参考になりました。

[*Rails*] deviseの使い方(rails5版) - Qiita

参考リンク

How To: Allow users to edit their account without providing a password · plataformatec/devise Wiki · GitHub

easyramble.com

yoshitsugufujii.github.io

rails: devise update user without password - CODE Q&A Solved

[*Rails*] deviseの使い方(rails5版) - Qiita

awkのprintでピリオドを出力したいが出来なかった

追記

結論、できた。ピリオドをダブルクォテーションで括ればよかったようです。

echo "1.2.3" | awk -F. '{print $1"."$2"."($3 + 1)}'

問題

下のようにawkのprintを使って演算した結果をピリオド付きで出力をしたかったが出来なかった

$ cat awk_test.sh
#!/bin/bash
echo "1.2.3" | awk -F. '{print $1.$2.($3 + 1)}'

$ sh awk_test.sh
124

\でピリオドをエスケープすればできる?と思ったけどもsyntax errorが出てしまう。

解決策

最初に加工した結果をスペース区切りで出力させて、そのあとsedでピリオドに置き換えた

$ cat awk_test.sh
#!/bin/bash
echo "1.2.3" | awk -F. '{print $1,$2,($3 + 1)}' | sed 's/ /./g'

$ sh awk_test.sh
1.2.4

Vue.js + Golangでタスクリスト作ってみた

Vue.jsとgolangを使って何か作ってみたかったので試しにタスクリストを作ってみました。

github.com

Vue.jsの書き方やgolangAPI実装はmattnさんのブログを大いに参考にさせて頂きました。

mattn.kaoriya.net

見た目はこんな感じです。

f:id:anrakusan:20180625070806p:plain

WebアプリケーションフレームワークはEcho、ORMにgormを利用しています。 また、Dockerの勉強も兼ねてフロントエンドとバックエンドの環境を立ち上げられるよう設定ファイルも書いてみました。

バージョン確認

$ go version
go version go1.10 darwin/amd64

$ node -v
v9.8.0

$ npm -v
5.8.0

なお、環境構築の手順は割愛します。

フロントエンドの実装

タスクリストの機能は次の通りです。 - タスクの一覧表示 - タスク作成 - タスクの完了、及び残タスクに戻す

上記の処理はsrc/components/Todo.vueにあります。 下のコードはjsが動く部分です。

<script>
import axios from 'axios'
export default {
  name: 'Todo',
  data () {
    return {
      newTask: '',
      todos: []
    }
  },
  created: function () {
    axios.get('http://localhost:1323/tasks')
      .then((response) => {
        console.log(response)
        this.todos = response.data.items || []
      })
      .catch((error) => {
        console.log(error)
      })
  },
  methods: {
    addTask: function (todo) {
      let params = new URLSearchParams()
      params.append('text', this.newTask)
      params.append('done', false)
      axios.post('http://localhost:1323/tasks', params)
        .then((response) => {
          this.todos.unshift(response.data)
          this.newTask = ''
        })
        .catch((error) => {
          console.log(error)
        })
    },
    updateTask: function (todo) {
      let params = new URLSearchParams()
      params.append('done', !todo.done)
      axios.put('http://localhost:1323/tasks/' + todo.id, params)
        .then((response) => {
          todo.done = !todo.done
          console.log(response)
        })
        .catch((error) => {
          console.log(error)
        })
    }
  }
}
</script>

created()はページをロードした時だけ実行されます。

また、データのやり取りはすべてAjaxAPIを実行して結果を取得するようにしています。

axios使いやすいです。

バックエンドの実装

バックエンドのAPIGolang + Echo + Gormで実装しています。

ミドルウェアやルーティングの設定はmain.goでして実処理はhandler.goに書いてます。

model/にはタスクのデータ定義やデータアクセスの処理を定義していて、ビジネスロジックは書かずシンプルにしています。

func (m *Model) FindAll() (tasks []model.Task, err error) {
    err = m.DB.Table(table).Find(&tasks).Error
    return
}

func (m *Model) Create(task model.Task) (t model.Task, err error) {
    t = task
    err = m.DB.Table(table).Create(&t).Error
    return
}

func (m *Model) Update(task model.Task) (t model.Task, err error) {
    t = task
    err = m.DB.Table(table).Where("id = ?", task.ID).Update("done", task.Done).Error
    return
}

実際のデータベース接続処理とも切り離していて、接続処理はdatabase/に定義しています。

func Connect() (*gorm.DB, error) {
    db, err := gorm.Open("mysql", "root:password@tcp(mysql:3306)/app?charset=utf8&parseTime=True&loc=Local")
    if err != nil {
        return nil, err
    }
    return db, nil
}

また、tasksのテーブル定義はinfra/mysql/sqls/1_init.sqlに記載してます。

環境構築

今回紹介したアプリはなるべくdocker-composeで再現できるようにしていますが、APIのビルドは事前に実行する必要があります。本当はそこもMakefile使う等で自動化したい。。

cd TodoList/backend/service
GOOS=linux go build -o api main.go handler.go

基本必要な記述はできるだけ個々のDockerfileではなくdocker-compose.ymlに寄せてます。

version: '2'
services:
  web:
    container_name: 'web'
    build:
      context: ../front/
      dockerfile: Dockerfile
    volumes:
      - ../front:/app
    networks:
      - front
    ports:
      - 8080:8080

  api:
    container_name: 'api'
    build:
      context: ../backend/service/
      dockerfile: Dockerfile
    networks:
      - front
      - datastore
    ports:
      - 1323:1323

  mysql:
    container_name: 'db'
    build:
      context: ./mysql/
      dockerfile: Dockerfile
    volumes:
      - ./mysql/mysql_data:/var/lib/mysql # データの永続化
      - ./mysql/sqls:/docker-entrypoint-initdb.d # 初期データ投入
    environment:
      - MYSQL_ROOT_PASSWORD=password #rootパスワードの設定
    networks:
      - datastore
    ports:
      - 3306:3306

networks:
    front:
    datastore:

webのDockerコンテナは起動時にnpm installとnpm runしてます。 apiコンテナは事前にlinux用にビルドされたバイナリを配置して起動しているだけです。 DBのテーブル定義などはコンテナ構築時に初期データを読み込むようにしています。

      - ./mysql/sqls:/docker-entrypoint-initdb.d # 初期データ投入

感想

なんだかんだで今回一番時間かかったのはdockerを使った環境構築。。。

Vue.jsを使ってみたくてアプリを作ってみましたが、ファイル分割しやすいしstyleとhtmlとjsをコンポーネント毎に分けられるので処理を理解しやすい。axiosなどの使いやすいライブラリもあるので開発するならVue.jsでいい感じ。レイアウトのデザインを作るためのライブラリも充実していてElementやVuetifyがあります。Vuetifyは簡単にマテリアルデザインが作れる感じなので使ってみたい。

【Flutter入門】Flutterでメモ帳アプリ作ってみた

そろそろ何かアプリを作ってみたいと思ったので簡単にマルチプラットフォームなアプリを作れるFlutterというフレームワークを触ってみました。 今回は勉強の一環として作成した簡単なメモ帳アプリを公開、解説します。 作成したアプリはGithubにアップしています。

github.com

コードの解説の前にFlutterについてですが、Flutterとはハイブリットアプリを開発するためのフレームワークDartという言語で実装されています。 Flutterの公式にも書いてありますが簡単にMaterial UIを持ったネイティブアプリを開発することができます。 flutter.io

書いててとても便利だったのがホットリロード機能で、コード修正→UI確認が秒速で完了します。

実際にアプリ開発に入る前に公式のチュートリアルは一度やってみた方がいいでしょう。 flutter.io

なお、アプリ開発のための環境構築方法については割愛します。

アプリの機能

今回実装したアプリの機能は次の通りです。

  • メモの一覧表示

  • メモの新規作成

  • メモの編集

  • メモの削除

main.dart(Home画面)

アプリのホーム画面でメモの一覧を表示させています。

アプリの全体はmain.dartに記述されており、下記のように定義しています。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Note App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new Home(title: 'Note List'),
    );
  }
}

class Home extends StatefulWidget {
  Home({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _HomeState createState() => new _HomeState();
}

アプリののタイトルと_HomeStateというWidgetを定義しています。 このWidgetはStatefulでメモ帳のデータを持たせています。

class _HomeState extends State<Home> {
  List<Memo> memos = new List<Memo>();

...

Home画面のUIは次のコードで定義されています。

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: _list(),
      floatingActionButton: new FloatingActionButton(
        tooltip: 'add note',
        child: new Icon(Icons.add),
        onPressed: () {
          Memo newMemo = new Memo('','');
          memos.add(newMemo);
          Navigator.push(context, new MaterialPageRoute<Null>(
              settings: const RouteSettings(name: "/create"),
              builder: (BuildContext context) => new Create(newMemo)
          ));
        }
      ),
    );
  }

ScaffoldというクラスでUIを構成する各コンポーネントを定義します。 それぞれのコンポーネントについて説明します。

  • appBar

アプリのナビゲーションバーに表示するタイトルです。

  • body

Home画面に表示するコンポーネントの本体。_list()でメモ帳の一覧を表示するためのリストが返されます。

  • floatingActionButton

右下に丸いボタンを出すことができます。押すとメモ帳の新規作成画面に遷移します。

        onPressed: () {
          Memo newMemo = new Memo('','');
          memos.add(newMemo);
          Navigator.push(context, new MaterialPageRoute<Null>(
              settings: const RouteSettings(name: "/create"),
              builder: (BuildContext context) => new Create(newMemo)
          ));

Navigator.pushで遷移先を指定します。また、遷移する際に新しいメモデータを配列に追加しています。 また、メモ帳のデータはMemo型を自作して配列にして保存するようにしています。 Memo型はentity/memo.dartで定義しています。

そしてメモ帳のリストを作成している箇所が次の通り。

  Widget _list() {
    return new ListView.builder(
        padding: const EdgeInsets.all(16.0),
        itemCount: memos.length,
        itemBuilder: (context, i) {
          final item = memos[i];
          return new Dismissible(
            key: new Key(item.title),
            onDismissed: (direction) {
              memos.removeAt(i);

              Scaffold.of(context).showSnackBar(
                  new SnackBar(content: new Text("Memo dismissed")));
            },
            // Show a red background as the item is swiped away
            background: new Container(color: Colors.red),
            child: new ListTile(
              title: new Text(
                item.title,
                maxLines: 1,
                style: _biggerFont,
              ),
              onTap: () {
                Navigator.push(context, new MaterialPageRoute<Null>(
                    settings: const RouteSettings(name: "/detail"),
                    builder: (BuildContext context) => new Detail(item)
                ));
              },
            ),
          );
        },
    );
  }

ListView.builderを使ってメモ帳のデータをListViewに表示させています。

メモ帳の要素を元にListTileを一つずつ作っています。

ListView内のListTileをタップしたらメモ帳の編集が行えるようにしたいのでonTapというイベントハンドラーを定義して、 押された時に編集画面に遷移するようにしてます。

また、親要素にDismissibleクラスを定義していますがこれはListViewのTileにスワイプ→削除のようなモーションを実装するのに必要になります。onDismissedがメモ帳削除のイベントハンドラーです。

create.dart(メモ帳の新規作成)

前述したメモ帳の新規作成画面はHome画面の右下に出てくる丸いボタンを押すと遷移します。 新規作成画面にはテキストフィールドとボタンを置いています。

@override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Create new note'),
      ),
      body: new ListView(children: <Widget>[
        new TextField(
            decoration: new InputDecoration(
                hintText:"Write title."
            ),
            controller: new TextEditingController(text: _memo.title),
            onChanged: (String newTitle) {
              _memo.title = newTitle;
            }
        ),
        new TextField(
            maxLines: 10,
            decoration: new InputDecoration(
                hintText:"Write memo."
            ),
            controller: new TextEditingController(text: _memo.body),
            onChanged: (String newBody) {
              _memo.body = newBody;
            }
        ),
        Center(
          child: new Container(
            margin: const EdgeInsets.all(10.0),
            height: 75.0,
            width: 150.0,

            child: new FlatButton(
              child: new Text('save memo'),
              color: Colors.lightBlue,
              textColor: Colors.white,
              onPressed: () {
                Navigator.pop(context);
              }
            )
          )
        )
      ])
    );
  }

2つのテキストフィールドはonChangedを実装していて、変更を行ったタイミングでメモ帳の内容を上書きしています。 また、ボタンを押した時のイベントは単に元の画面(Home画面)に戻っているだけです。

detail.dart(メモ帳の編集)

メモ帳の詳細画面は新規作成とほとんど変わらないです。Home画面に戻るためのボタンがないだけ。

メモ帳の削除

メモ帳の削除については前述した通り、main.dartでonDismissedというイベントハンドラーで実装しています。

            onDismissed: (direction) {
              memos.removeAt(i);

              Scaffold.of(context).showSnackBar(
                  new SnackBar(content: new Text("Memo dismissed")));
            },

ListTileをスワイプすると対応するインデックスのメモデータを削除しています。 UIも削除後に自動的に再描画されます。

この勝手に再描画してくれる点がとても楽。これが正しいやり方なのかは分からないけども。

実装してみての感想

公式ドキュメントも分かりやすく情報は十分にあるので問題なく実戦で使えると思う。 文法的な部分ではアプリのUI構造を宣言的に書けるのでその分構造を組みやすいと感じるが、階層が深くなると一覧性を損なってしまう点を強く感じた。

Widgetの記述ををメソッドやクラスを使って階層を減らすように工夫が必要そうだが、別Widgetの変数をどう参照するかとかも考えないといけない。

UIの再描画を伴うデータの更新はStatefulなWidgetがSetState()の中で行うのが正しいと思われるので、今回のようなデータのポインタを渡した先で値変更、というようなやり方は良くないのかもしれない。

このWidget同士でのデータの扱い方とかStatefulとStatelessのWidgetの使い分けは良く分かっていないので後で調べてみる。

ともあれ、やはり開発時のホットリロードはサクサク開発を進められるのですごく気持ちいいので今後もFlutterは触ってると思う。