おじゃまぷよ系エンジニアメモ

主にスマホネイティブ関連Tips。たまにWebも書きたい。お腹はぷよぷよ

BitriseでAndroidアプリのリリースを自動化

この記事はBitriseアドベントカレンダーの16日目の記事です。 仕事で何かとお世話になってるBitriseですがアプリで使うには便利なのでオススメしています。

Bitrise とは

Bitrise はモバイルアプリ開発に特化した CI サービスです。ブラウザ上でひとつひと つの step を組み合わせて Workflow と呼ばれる一連の流れを作成するところが簡単にで きることです。複雑なスクリプトを自分で書かずとも Bitrise があらかじめ用意している ので、利用者は使いたい step を選び、必要なパラメーターを設定するだけでいい感じの Workflow を作成できます。たとえば、「GitHub から clone」して、「Android アプリをビ ルド」し、「UnitTest を実行」し問題なければ「Google Play Store へリリース」という 流れを自分でスクリプトを一切書くことなくブラウザ上で組み立てることができます。

使用目的

弊社 においては主に以下の目的で使っています

  • staging 環境向けにビルドし実機テストをする
  • プルリクエストをトリガーに自動でテスト、deploygate へアプリの配布
  • release ブランチプッシュをトリガーに Google PlayiTunes Connect へ deployし、リリース作業の自動化

今回はその中でもプルリクエストのトリガーと release ブランチプッシュのトリガーの WorkFlow について紹介します。

releaseブランチプッシュ時のワークフロー図

現時点での弊社のワークフロー図です。

f:id:masahide318:20191215222620p:plain

ざっくり文章で説明すると

  1. Git Pullしてきます
  2. 前回releaseのリモートブランチが残ってないかチェックしています
  3. AndroidSDKをインストールします
  4. GradleでassembleProductReleaseでリリース用のapkファイルを出力します
  5. Bitrise上にdeployします
  6. Bitrise上にdeployしたapkのインストールページのQRコードを出力します
  7. Google Playへβ版としてdeployします
  8. SlackにGoogle Playへのdeployが成功したことと、6で出力した本番アプリ確認用のQRコードをSlackで通知します

いくつかStep紹介

f:id:masahide318:20191215223448p:plain

前回のreleaseのリモートブランチが残ってないかチェックするスクリプトです。
たまにリリース後にリモートブランチが残ってることがあり、本当にdevelopとmasterにマージ済みかどうか不安なのでチェックするようにしました。
Bitriseは生のスクリプトも書けるので用意されているStepにはないものはこうやってスクリプトを書くことで対応可能なところも柔軟性があっていいですね。

#!/usr/bin/env bash
git fetch --all
branchCount=(`git branch -a | grep remotes/origin/release/ | wc -l`)
if [ $branchCount -gt 1 ] ; then
  echo "releaseブランチが2つ以上pushされています。古いリリースブランチがリリースされているか確認してください"
  exit 1;
else
  exit 0;
fi

f:id:masahide318:20191215223759p:plain f:id:masahide318:20191215223956p:plain

Google Playへアップロードするapkファイルを出力するStepです。
プロダクトフレーバーを導入している場合はassembleProductReleaseコマンドを入れましょう

f:id:masahide318:20191215231532p:plain

Google PlayへdeployするStepです。
このステップを実行するにはBitrise上に2つ登録しておく必要があります。

  • keystoreファイルとそのaliasとパスワード
  • Service Account’s JSON key file

keystoreファイルについては説明を省きます。AndroidStudioから作成しましょう。
Service Account’s JSON key fileはGoogle PlayのDeveloperのコンソールから作成します。
詳しくはこちらの記事を参考に作成します。
作成したファイルは BitriseのCode Signingタブから登録しましょう。

f:id:masahide318:20191215224758p:plain

Slackへdeployが完了したことを通知するStepです。

f:id:masahide318:20191215224814p:plain

Create Install Page QR Code」のステップを途中に入れておくと「$BITRISE_PUBLIC_INSTALL_PAGE_QR_CODE_IMAGE_URL」の変数にインストールページへのQRコードを参照できるのでSlackのStepの「A URL to an image file that will be displayed inside the attachment」にそのまま入れてあげると

f:id:masahide318:20191215224824p:plain

このようにSlackにQRコードが来ます。開発者以外の人に最後動作確認してもらったりと、リリース前に誰かに触ってもらいたいときにQRコードを伝えます。

さいごに

ものすごくざっくりですがGoogle Playへdeployするまでのワークフローを紹介しました。 実際はdeploy前にInstrumentalテストを入れたいのですが、UIテストがTestLab上でうまく動かないことがあるので一旦おあずけしています。
Instrumentalテストが安定して動作するプロダクトの場合は間にいれておくと良いかも知れません。
技術書典7のときBitriseのチュートリアル本を書きましたのでよろしければこちらもどうぞ。

t-masahide.booth.pm

技術書典6出典レポート

技術書典6に出典してきました。
現地に行くまで実際の本がちゃんと出来上がっているのかのドキドキはなれませんね。 f:id:masahide318:20190501134716j:plain f:id:masahide318:20190501134742j:plain 前回のテストが書けない人のためのAndroid MVP に続いて今回はBitriseで始める Android CI環境というタイトルで書きました。 今回の本は実際に会社のプロダクトでBitriseを運用するにあたりどういうWorkflowを組んでいるのかを公開しつつ、基本的なStepやあると便利なStepを紹介しています。 たとえば、gitのpushをトリガーにUnitTestを回したり、deploygateでテストアプリ配布したり、そのままGoogle Playへデプロイする流れやdangerと連携して自動でプルリクにコメントを付けてくれるStepなど色々紹介しております 。 Bitriseの基本的な使い方から運用フローにのせたときにどういうWorkflowがあると良いかを紹介してますので、ネイティブアプリのCIツールとして検討している方や、Bitriseそのものに気になっている方はぜひご購入してみてください。

t-masahide.booth.pm

目次は以下のようになっております。

f:id:masahide318:20190501135948p:plain f:id:masahide318:20190501140026p:plain

今回で出典は2回目なのですが「前回の本を購入してよかったからまた買いに来ました」なんて声を書けてくれる方もおり本当に感謝してるのと同時に、リピートしてくれる人がいると思うとその方たちの期待を裏切らないように少しでも人の役に立つものを書かねばという緊張感も出てきました…。 次回も出典するかどうか未定ですがもし出典すれば場合はよろしくおねがいします。

技術書典5をMarkdownのみで執筆してもなんとか黒字になった話

技術同人誌 Advent Calendar 2018 14日目の記事です。

本当はなぜこの本を書いたのかの深掘りとかしようと思いましたけど、本についての想いはこちらのブログに込めておきました。
熱い思いを込めておきましたので読んでください。

www.wantedly.com

もう技術書典については書くこともなくなってきたので総まとめ的な感じで執筆環境から入稿作業や売上どうだったか的なところを備忘録を込めて書いていきます。

執筆環境

VSCodeMarkdownで執筆していました。 文章の校正にredpenを最初使ってたんですが、途中でVSCodeプラグインテキスト校正くんというものを発見してからこちらを使ってました。

基本的にはVSCode上でMarkdownpreview見ながらがーっと書いて
ある程度書けたらmd2reviewでREView形式に変換してpdfを出力していました。
Re:Viewのstyleをいい感じに変更したりする技術がなかったので、デフォルトのstyleのままRe:View記法を自分で書くことなくMarkdownのみで書き続けました。 (marginだけいじったかも?) 今回書いてある程度Re:View記法も覚えてきたので、次回は最初はMarkdownを使わずに書いてみようかと考え中。
本のレイアウトとかもいじれるようになりたい…

わからないことだらけの入稿作業

ねこのしっぽさんを利用して最終的に出来上がったpdfや表紙画像を入稿しました。
正直なにもわからなかったので、求められるがままに本文pdfや表紙画像(psdファイル)をアップロードしました。
ただその後のフォローが手厚くて、pdf内で貼り付けていた画像がおかしいところや、表紙の画像のおかしいところなど細かい部分まで目を通してくれていてわざわざ電話にて教えてくれました。
最終的にはMarkdown->review記法に変換したもので、style.cssも特にいじらない本当にデフォルトのままでも無事に入稿できました。 ので最初はレイアウトにさえこだわらなければ最初から最後までMarkdownで書くことも可能です。
お値段は早期割引ありで150部 66ページで40,320円でした。思ったより安かったです。
入稿するときに表紙や印刷方法など何から何までわからなかったので、ほぼデフォルトのまま依頼したんですが、ちゃんと思ったとおりの本になってましたので、そのときのオプションも参考までに載せておきます。
出来上がった本は当日現地に直接届ける方式だったので、完成品が当日になってみないとわからないという恐怖と戦いました。

f:id:masahide318:20181213112825j:plain f:id:masahide318:20181213112838p:plain

被チェック数と売上

最終的にイベント時の売上は被チェック数が76で当日の売上部数は90部ほどでした。 他の方のブログを見てると被チェック数が150ぐらいあっても100部も売れなかったパターンなどあるらしいので、自分の場合は本当に運が良かったなと思います。
「友人に100部ぐらいは結構イケるよ」って言われたのを鵜呑みにして調子に乗って150部刷ってしまったので本当に危なかった。
爆死を避けられた理由は恐らく、「表紙で目を引く」と「声をかける」の2つだと思います。
まず表紙は小菅かつきさんに圧倒的美少女を描いてほしいとお願いしてめちゃくちゃ良い表紙を作ってもらいました。緑ベースにしたのはドロイド君がバ美肉したという設定で、周りには過去のAndroidバージョンのお菓子たちが散りばめられています。
そして現場ではとりあえず手にとって試し読みしてくれてる人にひたすら「これはこういう本です」というテンプレ説明を繰り返すマシーンと化して熱い思いを伝えていました。軽く会話してどういうことに困ってるかなど聞いたりして「だったらこの本参考になるかもしれないです!」みたいな流れで必死にアピールしてました。1年分のコミュ力を使い果たした場面。
なので手にとってから購入までのコンバージョン率は高かったように思います。
現在BOOTH上でオンラインで販売しておりますが、かれこれイベントが終わってからも20冊ほど売れています。
在庫抱えてるので買ってもらえると助かります。markdownのみで書いたものがどんな感じか見たい方もぜひ… ! t-masahide.booth.pm

というわけで生々しい売上話でした。
発行部数さえミスらなければ大赤字にはならないと思います。
中には基本電子版のみ販売してリアル本は見本分だけ用意するという方法をとってるサークルもありましたので、経費を抑えたい場合はそういうやり方もアリですね。

さいごに

途中にも書いてますがレイアウトにこだわらなければMarkdownで書き通すことも可能なので
あまり躊躇せずにとりあえずやってみることが大事ですね。 個人的には入稿作業が最もハードル高かったです…完全に未知の世界だったので。
執筆は余裕を持って終わらせて入稿しましょう。

PHPで学ぶChain of Responsibilityパターン

この記事はPHPで学ぶデザインパターン Advent Calendar 2018の4日目の記事です。

qiita.com

Chain of Responsibilityパターンとは

よく言われるのがたらい回しパターンですね。
自分では処理できない場合次のオブジェクトに処理をお願いするパターンです。

サンプルコード

とりあえずサンプルコードです。
RPGを想定して、勇者がモンスターと戦いますがそのモンスターのレベルが高かったり強い種族の場合は自分より強い勇者に倒すのをお願いするものを想定しています。

/**
 * モンスターです。名前とレベルだけ持ってます
 */
class Monster
{
    private $name;
    private $level;

    public function getName()
    {
        return $this->name;
    }

    public function getLevel()
    {
        return $this->level;
    }

    public function __construct($name, $level)
    {
        $this->name = $name;
        $this->level = $level;
    }
}
/**
 * 抽象的な勇者です。
 * canDefeat()で倒せるかどうかの判定は具体的なサブクラスに自由に決めてもらいます
 */
abstract class Braver
{
    private $name;

    /**
     * @var Braver
     */
    private $next;

    public function __construct(String $name)
    {
        $this->name = $name;
    }

    abstract function canDefeat(Monster $monster);

    /**
     * たらい回し先をセットします
     */
    public function setNext(Braver $next)
    {
        $this->next = $next;
        return $this->next;
    }

    /**
     * モンスターと戦います。
     * 自分が倒せない相手だと次の勇者に任せます
     */
    public function fight(Monster $monster)
    {
        if ($this->canDefeat($monster)) {
            echo $this->name . " が " . $monster->getName() . " を倒した\n";
        } else if ($this->next != null) {
            $this->next->fight($monster);
        } else {
            echo "全滅した\n";
        }
    }
}

3種類の勇者を用意しました。

/**
 * 新米勇者
 */
class NewcomerBraver extends Braver
{

    /**
     * レベル10未満のスライムなら倒せる
     */
    function canDefeat(Monster $monster)
    {
        return $monster->getName() == 'スライム' && $monster->getLevel() < 10;
    }
}

/**
 * 中級勇者
 */
class IntermediateBraver extends Braver
{

    /**
     * どんなモンスターもレベル15未満なら倒せる
     */
    function canDefeat(Monster $monster)
    {
        return $monster->getLevel() < 30;
    }
}

/**
 * ベテラン勇者
 */
class ExpertBraver extends Braver
{
    /**
     * どんなモンスターもレベル50未満なら倒せる
     */
    function canDefeat(Monster $monster)
    {
        return $monster->getLevel() < 50;
    }

}
$newComerBraver = new NewcomerBraver("新米勇者");
$intermediateBraver = new IntermediateBraver("中級勇者");
$expertBraver = new ExpertBraver("ベテラン勇者");
//新米勇者->中級勇者->ベテラン勇者の順にたらい回しする
$newComerBraver->setNext($intermediateBraver)->setNext($expertBraver);

$newComerBraver->fight(new Monster("スライム", 9));//新米勇者 が スライム を倒した
$newComerBraver->fight(new Monster("ドラキー", 9));//中級勇者 が ドラキー を倒した
$newComerBraver->fight(new Monster("スライム", 20));//中級勇者 が スライム を倒した
$newComerBraver->fight(new Monster("スライム", 40));//ベテラン勇者 が スライム を倒した
$newComerBraver->fight(new Monster("りゅうおう", 99));//全滅した

どういう時につかうの?

複雑なif-elseやswitch文を見た時に、これはポリモーフィズムで解決できないか?と少し立ち止まってみます。 今回のサンプルを愚直に実装してみると(正確にはちょっと違うけど…)

$monster = new Monster("スライム", 10);
$newComerBraver = new NewcomerBraver("新米勇者");
$intermediateBraver = new IntermediateBraver("中級勇者");
$expertBraver = new ExpertBraver("ベテラン勇者");

if ($monster->getName() == 'スライム' && $monster->getLevel() < 10) {
    $newComerBraver->fight($monster);
} else if ($monster->getLevel() < 30) {
    $intermediateBraver->fight($monster);
} else if ($monster->getLevel() < 50) {
    $expertBraver->fight($monster);
} else {
    echo "全滅した";
}

だいたいこのような感じになります。
これがぐらいなら愚直に実装した方がわかりやすいかもしれませんがもう少し条件が複雑になったり if-elseのパターンが追加されると見通しが悪くなります。そして

 if ($monster->getLevel() < 50) {
    $newComerBraver->fight($monster);
} 

みたいに条件と内部の処理が一致しないミスなんかも起こるかもしれません。 CORの良いところの1つに、条件と処理がひとつのクラスにまとまっているところですね。

もう少しシンプルに

CORをデザインパターンそのまま組むのはちょっとしんどいのでもう少しシンプルにした形で
配列に入れてforeachで回して書くCORパターンもどきを好んで使います。
これだと条件が増えても新たにBraverをextendsしたクラスを用意して配列にaddするだけでいいので楽です。

$braverParty = [
    new NewcomerBraver("新米勇者"),
    new IntermediateBraver("中級勇者"),
    new ExpertBraver("ベテラン勇者")];

$monster = new Monster("スライム", 10);
foreach ($braverParty as $braver) {
    if ($braver->canDefeat($monster)) {
        $braver->fight($monster);
        return;
    }
}
echo "全滅した";

かなりざっくりした説明になりましたが、if-elseやswitch文を見た時にポリモーフィズムでシンプルに書きたいという欲求に駆られたときに思い出してみるのもいいかもしれませんね。

参考資料

www.dezapatan.com

isucon8本戦に出たものの惨敗しました

ものすごくありがたいことにisucon8の本戦まで出場することできました。 ぶっちゃけチームメンバーが優秀すぎて自分はほとんど何もできなかったけど!

本戦出場の証。アイコンは先日の技術書典5で出した本のキャラクター。 f:id:masahide318:20181022194854j:plain

本戦でやったこと

クエリ改善

SELECT ORDER BYでフルスキャンするクエリをLIMIT 1つけて改善する。 しかしPHPのソース的にパフォーマンスにはそこまで影響しなかった。

諸々キャッシュしようとする

とりあえずredisで最高買値と最高売値をキャッシュしようとする
キャッシュしたと思ったらベンチマークコケてうまくいかない…キャッシュ削除のタイミングとかいろいろ悪かったかもしれない。 泥沼にハマりそうだったので途中で諦める (ほんと時間を無駄にした…申し訳ない)

N+1問題に対応

for文ぶんまわしでパフォーマンス悪いところがあったので
SQLのクエリ修正したり、INDEX貼り直したりする。プログラム側のデータの詰替えも修正する。 これでちょっとスコアあがる。2200ぐらいになった。

他のメンバーの修正をマージする

ぶっちゃけ自分が出来たのはこれぐらいで、あとは他のメンバーの修正をマージしてフィニッシュしました。 DB専用サーバーを1個別サーバーに設けたり、nginx側でWebサーバー負荷分散したりしましたが最終的に4200ほどに落ち着きました。 最後ベンチマークうまく動かず超ギリギリで最後動いたw f:id:masahide318:20181022200901p:plain

反省点

反省が多すぎて困る…

  • インフラはチームメンバーにお任せしていたが僕はdockerになれてないのですでに厳しかった。
  • キャッシュは早々に諦めるべきだったなぁと…そもそも効果うすそうだったし。
  • logのbulk_sendの存在に終了30分前ぐらいに気づいた…辛い
  • enable_shareの存在に気づかず最後まで放置してた
  • 途中でDataGripからisuconサーバーのmysqlに繋がらなくなりパニック

感想

  • イベントは予選は本戦もめっちゃ楽しかったです。休日潰してでも行く価値あり!
  • 本戦振り返ってみるともっと色々できただろ!って反省が多くてしばらくずっと悔しい思いしてましたw
  • でも予選通過は17倍ぐらいの倍率だったらしいので、初めてのisucon挑戦で本戦まで行けただけでも良かったとしよう!
  • dockerを捨ててるチームがあって、なるほどな…って思った。めっちゃ良い判断だと思った
  • ちゃんとソースとドキュメントは読もう!思わぬ落とし穴が潜んでるぞ!

カップケーキ美味しかったです。その他のケータリングもどれも美味しくて運営さまには感謝です。 f:id:masahide318:20181022204555j:plain

技術書典5に「テストが書けない人のためのAndroid MVP」を出展してきました

技術書典5無事に終わりました。足を運んでくださった皆様ありがとうございます。
「テストが書けない人のためのAndroid MVP」という本を出展していました。
f:id:masahide318:20181008200153j:plain
f:id:masahide318:20181008203441j:plain

そもそもなぜこの本を書こうとしたのか

事の発端はDroidKaigiに行った時に、なんとなくチームメンバーとテストコード書きたいなぁとざっくりした話になって
クリーンアーキテクチャやMVVMやMVPがある中で最終的に特別なライブラリを必要とせず、設計上やりすぎ感なく丁度いいであろうという理由でMVPに決まりました。
とはいえ実際にMVPっぽく書いてみたものの結構人によってMVPの認識の違いがあったり、元々存在する便利なシングルトンやUtilクラスが邪魔でテスト書きたいけどうまくいかない部分が出てきたのでそのあたりを解決するにはどうすればいいのかと考え、やってみた結果をこの本にまとめました。
DIとか使えばいいじゃんっていう安直な考えをせず、普通にプログラムを書いていかにJUnitでテストを書けるようにするかがコンセプトになってます。
レガシーコード改善ガイドを参考にしたものが多いのですが、Androidのあるあるな依存を排除するのにかなり参考になる部分が多かったので助かりました。

本書で書ききれなかったこと

本書ではマッチョなActivityをまずはMVPに置き換えてみるということを主軸にしたので、その後の発展としてMVPで依存を切り離したものMVVMに移行したいときはどうするかなど、その後の発展部分については書けなかったのでどこかで余裕があれば電子版などに追加していきたいと考えております……

出展してみての感想

今更MVPは若干古いのであまり多くの人にウケるものではないですが、話を聞いているとやはりテストコードを書けない人やActivityにがっつりソースコード書いてあるプロジェクトはまだまだありそうだなぁという印象を受けました。
ただ自分の書きたいものが書けたので大変満足しております。

オンライン販売もしております

BOOTHさんにて電子版と物理本も販売しておりますので、レガシーAndroidコードと戦っていて気になった方はどうぞこちらからお買い求めください。

t-masahide.booth.pm

ktlintをpre-commitに引っ掛けてコードフォーマットする

元々php書いてて、php-cs-fixerをpre-commitでフックして整形してたなぁ…と思って最近Androidをkotlinでソース書いてるときに、同じようなやつkotlin版ないのかなぁと調べたらちゃんとありましたね。

github.com

ということでphp-cs-fixerのパターンと同様にktlintでフォーマットかけます pre-commitのscriptはこちらを参考にしました

blog.manaten.net

結果以下のようなpre-commitにしました

#!/bin/sh

if git rev-parse --verify HEAD >/dev/null 2>&1
then
    against=HEAD
else
    # Initial commit: diff against an empty tree object
    against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
# Redirect output to stderr.
exec 1>&2

# コミットされるファイルのうち、.ktで終わるもの
for FILE in `git diff-index --name-status $against -- | grep -E '^[AUM].*\.kt$'| cut -c3-`; do
    /usr/local/bin/ktlint -F $FILE
    git add $FILE
done

以上でとりあえずpre-commitでktlintによるコードフォーマットが行われるのでちょっと便利