Re:birth エンジニアリング

Flultterとテックブログと時々iOS

入社してから1年が経ったのでこの1年を振り返る

今日はただのポエムを書きました。

前置き

今の会社に転職してから1年が経ったので、この1年間について振り返ります。まさか1年前の自分が今リモートワークをしているなんて想像していなかったね。 本当は1年前の今頃「5年続けたフリーランスを辞めて Web 系自社サービスの会社に転職しました」みたいな転職エントリーを書こうと思っていた時期がありました。当時は法人を持っていたので代表者の住所変更等で法務局に何度も足を運んでいたり事務手続きに忙殺されたのでその機会はありませんでした。

概要と経歴

多分、初めてこのブログに来られる方もいらっしゃるかもしれませんので簡単に自己紹介をします。 モバイルアプリ開発を仕事にやっている Tamappe と言います。 iOS エンジニア歴は2014年からやってるのでもう7年になります。 Android 歴は1年弱です。その前は営業をしていましたので異業種からの転職になります。

7年間のうち5年間は法人を持ちながらフリーランスみたいな活動をしていました。法人はファイナンス的な意味と自社サービスを持ちたかった願望で作りました。法人は「税務関係」ではデメリットになりますが、「信用」においては圧倒的に有利と認識して作りました。そんな法人ですが、会社員に戻ったタイミングで休眠させて実質畳みました。

会社員に戻った理由

2019年4月に会社員に戻りました。世間では転職に該当します。 3ヶ月ぐらい前までよく色んな方から「よく会社員に戻りましたね」なんて言われましたが、今となっては良い判断だったなと思います。 会社員に戻る決意をしたのは2019年1月のお正月がきっかけです。

理由は2つありました。

簡単にいうと、今付き合っている彼女から「会社員に戻って欲しい」と告げられてしまったからです。これが1つ目の理由。 2つ目は2020年は「嫌な気配」がしたからです。2020年はオリンピック開催年ですね。

IT業界は2019年当時はめちゃくちゃ好景気で、誰がどう見てもイケイケドンドンのムードでした。さらに来年の2020年はオリンピック開催年だから景気のピークがマックスに来る年でもありました。だからこそ、オリンピックが終わったら景気が一気に冷えるなんて言われていました。

ただ、そこに落とし穴があるとは思っていたんですね。

景気の循環サイクルと2018年のリーマンショックが起きた時を思い出すと、2019年当時が景気のピークだと仮定すれば次に来るのは不況になります。

f:id:qed805:20200404185049p:plain
景気の波

ただこういった不況はリーマンショックの事を思い出すと皆が思っているより「ワンテンポ」早く来ることが特徴的です。 皆が2020年のオリンピックが終了した後と読んでいたみたいなので私はオリンピックが来る前と読みました。ですので、2020年が始まる前にはなんとかしておきたいという思いですね。

さすがにリーマン・ショック級の不況が訪れると生き残れないよなぁと思いましたので、「思い立ったら吉」ということで転職活動を始めました。そのため、私の場合は彼女のその一言が背中を押してくれた感じになりました。

今振り返るとそれからの2019年1-3月は怒涛の3ヶ月だったと思います。 その3ヶ月で物凄く運良く今の会社に転職することができ、毎日死ぬ思いでサービスを改善しています。

入社が決まってからの業務内容

やっと本題に入れますが、今の会社に入社してから今月で1年経ちました。たった1年ですけど、もう2年分は働いた感じです。 主な業務内容は既存サービスのモバイルアプリの改善業務です。 一応、私の持ち味はリファクタリングや技術的負債の返済だと思っていますので、最初はそのようなタスクをメインに取り組んでいました。 今はリモートワークでも開発できる環境を整えるタスクをメインにしています。

今、世間ではコロナウイルスで騒がれていてあとどれくらいで収束するのかが議論されていますね。あんまり、コロナの話しは触れたくないですが、某情報を追っているとこれはもう2, 3ヶ月で収束はできなそうな感じです。1, 2年単位で続くみたいな感じらしいので(アフターコロナと呼ばれています)、今回を機会に整備できればいいなと思い歯を食いしばっています。

仕事面ではお世辞抜きでいいメンバーの中で開発できていて、苦しい思いはしていますがやり甲斐のあるタスクをさせてもらっています。 ブログやツイッターでは Flutter での開発の情報発信をしていますが、本業では iOS は Swift 、 Android は Kotlin で書いています。 なんで Flutter の情報発信をしているかについては既に過去記事に書いていますのでそれを読んでいただければと思います。

blog.tamappe.com

あとはiOSのSwiftはもう凄い人が多すぎて、ここで情報発信のポジションを取りに行こうにも取れないやと諦めていることが一番大きいです。 Flutter も凄い人が沢山いますが、情報量ではまだ可能性がありそうかなと。

なので、業務後は余裕があるときに Udemy で Flutter の動画を視聴したり、 Flutter のウィジェットを調べたりと頑張っています。ここはいずれ何かしらでマネタイズすることを考えていたりしていなかったりです。

今後についての予定も書こうと思いましたが、記事の執筆から既に2時間が経ちますので今日はこれまでにします。

【AppleMusicクローン】クラスの分離を行う

前回のAppleMusicクローンアプリの続きです。

前回はmain.dartにレイアウトをただ並べて行きました。 その結果、一つのクラスでいろいろなクラスがごちゃまぜになりました。

blog.tamappe.com

今回はこのまま次の行の横スクロールを実装していくと メンテナンスが大変になることを想定してレイアウトの分離を行います。

前回はヘッダーとListViewを作成しましたので「ヘッダー」部分と「ListView」部分を分けたいと思います。

  • custom_sliver_appbar.dart (ヘッダー)
  • custom_sliver_list.dart (ListView)

命名でクラスを作成します。

ヘッダー部分のソースコード

custom_sliver_appbar.dart

import 'package:flutter/material.dart';

class CustomAppBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SliverAppBar(
      pinned: false,
      backgroundColor: Colors.white,
      expandedHeight: 40.0,
      flexibleSpace: FlexibleSpaceBar(
        titlePadding: EdgeInsets.only(left: 20),
        centerTitle: false,
        title: Text(
          '見つける',
          style: TextStyle(color: Colors.black),
        ),
      ),
    );
  }
}

ListView部分のソースコード

custom_sliver_list.dart

import 'package:flutter/material.dart';

class ContentSliverList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SliverList(
      delegate: SliverChildBuilderDelegate(
            (BuildContext context, int index) {
          if (index == 0) {
            return Padding(
              padding: const EdgeInsets.all(5.0),
              child: Container(
                height: 250,
                child: ListView(
                  // This next line does the trick.
                  scrollDirection: Axis.horizontal,
                  children: <Widget>[
                    Padding(
                      padding: const EdgeInsets.all(5.0),
                      child: Container(
                        width: MediaQuery.of(context).size.width - 40.0,
                        color: Colors.red,
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.all(5.0),
                      child: Container(
                        width: MediaQuery.of(context).size.width - 40.0,
                        color: Colors.blue,
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.all(5.0),
                      child: Container(
                        width: MediaQuery.of(context).size.width - 40.0,
                        color: Colors.green,
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.all(5.0),
                      child: Container(
                        width: MediaQuery.of(context).size.width - 40.0,
                        color: Colors.yellow,
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.all(5.0),
                      child: Container(
                        width: MediaQuery.of(context).size.width - 40.0,
                        color: Colors.orange,
                      ),
                    ),
                  ],
                ),
              ),
            );
          } else {
            return Card(
              child: ListTile(
                title: Text("list item:$index"),
                leading: Icon(Icons.photo),
              ),
            );
          }

        },
        childCount: 20,
      ),
    );
  }
}

main.dartの部分

main.dart

import 'package:apple_music_clone/widgets/content_sliver_list.dart';
import 'package:apple_music_clone/widgets/custom_sliver_appbar.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(),
      home: HomePage(title: '見つける'),
    );
  }
}

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

  final String title;

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

class _HomePageState extends State<HomePage> {
  bool _isVisibleHeader;
  ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    _scrollController.addListener(_scrollListener);
    _isVisibleHeader = true;
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  void _scrollListener() {
    print(_scrollController.offset);

    if (_scrollController.offset > 100) {
      setState(() {
        _isVisibleHeader = true;
      });
    } else {
      setState(() {
        _isVisibleHeader = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        elevation: _isVisibleHeader ? 1.0 : 0.0,
        backgroundColor: Colors.white,
        title: Visibility(
            visible: _isVisibleHeader,
            child: Text(
              '見つける',
              style: TextStyle(color: Colors.black),
            )),
      ),
      body: CustomScrollView(
        shrinkWrap: false,
        controller: _scrollController,
        slivers: <Widget>[
          CustomAppBar(),
          ContentSliverList()
        ],
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

ソースコードをビルドするとコンテンツ部分のセクション1が横スクロールできます。

f:id:qed805:20200401002359p:plain
ビルド時の挙動

次回からはcustom_sliver_list.dartにどんどんレイアウトを乗せていく予定になります。 セクションの概念を導入する予定なのでDartenum を使ってセクション分けしていきます。

【AppleMusicクローン】SliverAppBarとSliverListでヘッダーの伸び縮みを再現する

今回からApple Musicの真ん中のタブに存在する「見つける」の画面を実装していきます。

f:id:qed805:20200324234828p:plain
このヘッダーの部分の実装

今回、使うウィジェット

  • SliverAppBar
  • SliverFixedExtentList
  • ScrollController

この3つがメインになります。

Slivers について

SliverAppBar は Slivers の一部のウィジェットらしいです。

Slivers の説明 flutter.dev

A sliver is a portion of a scrollable area. You can use slivers to achieve custom scrolling effects.

Sliver はスクロール可能領域をカスタムにすることができるウィジェットとのことです。

おおまかな使い方はCustomScrollViewにSliverXXX と付くウィジェットを乗せていきます。

CustomScrollView(
        shrinkWrap: false,
        slivers: <Widget>[
          /// Sliverの付くウィジェットを乗せる
        ],
      )

SliversのAppBar に該当するウィジェットSliverAppBar、body に該当するウィジェットSliverListSliverGridなどが存在します。 それらをCustomScrollViewのslivers に乗せていきます。

今回のヘッダーを作成する場合にはSliverAppBarとSliverFixedExtentListを使います。

SliverAppBar(
            pinned: false,
            expandedHeight: 40.0,
            flexibleSpace: FlexibleSpaceBar(
              title: Text(
                '見つける',
              ),
            ),
          )
SliverFixedExtentList(
            itemExtent: 200.0,
            delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                return Container(
                  alignment: Alignment.center,
                  color: Colors.lightBlue[100 * (index % 9)],
                  child: Text('list item $index', style: TextStyle(fontSize: 30),),
                );
              },
            ),
          )

このように使います。

ScrollController について

ですが、Sliversを使っても「見つける」文字がスクロールして左側から中央に表示させることが難しかったです。 上級者であればスムーズに実装することができるのかもしれませんが、私は今回は別のアプローチを取りました。 スクロール量に応じて表示の切り替えをするアプローチを取ります。

Flutter でスクロール量を計算できるものにScrollControllerが存在するのでこれを使います。

使い方は

ScrollController _scrollController;

@override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
  }

として初期化します。 初期化した_scrollControllerをCustomScrollViewのcontrollerに設定します。

CustomScrollView(
        shrinkWrap: false,
        controller: _scrollController,
        slivers: <Widget>[
        ],
      )

そして、スクロールしたときに処理する内容をメソッドにしてaddListenerに入れます。

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    _scrollController.addListener(_scrollListener);
  }

  void _scrollListener() {
    print(_scrollController.offset);
  }

_scrollController.offset は現在のスクロール位置を出力します。 int型で数字なのでif文を使って制御します。

  void _scrollListener() {
    print(_scrollController.offset);

    if (_scrollController.offset > 100) {
      setState(() {
      });
    } else {
      setState(() {
      });
    }
  }

これでスクロール位置によって何かを制御できるようになりました。

ソースコードについて

それではこれらの機能をもとにして「見つける」の伸び縮みを再現したソースコードを書きます。 今回はmain.dart だけになります。

main.dart

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'サンプルアプリ',
      theme: ThemeData(),
      home: HomePage(title: '見つける'),
    );
  }
}

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

  final String title;

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

class _HomePageState extends State<HomePage> {
  bool _isVisibleHeader;
  ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    _scrollController.addListener(_scrollListener);
    _isVisibleHeader = true;
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  void _scrollListener() {
    print(_scrollController.offset);

    if (_scrollController.offset > 100) {
      setState(() {
        _isVisibleHeader = true;
      });
    } else {
      setState(() {
        _isVisibleHeader = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        elevation: _isVisibleHeader ? 1.0 : 0.0,
        backgroundColor: Colors.white,
        title: Visibility(visible: _isVisibleHeader, child: Text('見つける', style: TextStyle(color: Colors.black),)),
      ),
      body: CustomScrollView(
        shrinkWrap: false,
        controller: _scrollController,
        slivers: <Widget>[
          SliverAppBar(
            pinned: false,
            backgroundColor: Colors.white,
            expandedHeight: 40.0,
            flexibleSpace: FlexibleSpaceBar(
              titlePadding: EdgeInsets.only(left: 20),
              centerTitle: false,
              title: Text(
                '見つける',
                style: TextStyle(color: Colors.black),
              ),
            ),
          ),

          SliverFixedExtentList(
            itemExtent: 200.0,
            delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                return Container(
                  alignment: Alignment.center,
                  color: Colors.lightBlue[100 * (index % 9)],
                  child: Text('list item $index', style: TextStyle(fontSize: 30),),
                );
              },
            ),
          )
        ],
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

SliverFixedExtentList は今後別のウィジェットにする予定ですが、仮置きで書きました。 これで一番最初の難関を突破した気分です。

【クイズアプリ開発】復習するときの目次

これまでクイズアプリ開発をしてきましたので、わかりやすいように目次のページを作りました。

Flutterで4択クイズアプリのレイアウトを実装する

blog.tamappe.com

定数クラスの作成とウィジェットの共通化

blog.tamappe.com

選択肢のボタンを整える

blog.tamappe.com

Flutterでリセット画面を実装する

blog.tamappe.com

リセット画面で学ぶColumnのmainAxisAlignmentについて

blog.tamappe.com

リセット画面のデザインを整える

blog.tamappe.com

正解したときの処理と正答数の反映

blog.tamappe.com

次回は次のアプリのデザイン案を紹介する予定です。