未熟学生エンジニアのブログ

TetsuFeの個人開発ブログ

TetsuFeはテツエフイー と読みます。FlutterやWeb周り全般、チーム開発について語るブログ

zshのプロンプトにgitのブランチ名とステージング状態を表示する

.zshrcに以下を追記し、$ source ~/.zshrcを実行する

export GIT_PS1_SHOWCOLORHINTS=1
export GIT_PS1_SHOWDIRTYSTATE=1
git_prompt_sh=/usr/local/etc/bash_completion.d/git-prompt.sh
if [ -e $git_prompt_sh ]; then
 source $git_prompt_sh
 precmd () { __git_ps1 "%F{yellow}%c%f" " $ " }
else
 PS1='%F{cyan}%c%f \$ '
fi

こんな感じで表示されます。

f:id:swiftfe:20220201224126p:plain

Flutterの公式リポジトリに2文字だけコミットした話

先日Flutterの公式GitHubリポジトリに僕が作成したPRがマージされました。たった差分2文字だけですが、いつもお世話になっているフレームワークに少しでも貢献できたことはやはり嬉しいです🚀

実際のPR

github.com

内容はpackages/flutter/lib/src/material/text_theme.dartのコードコメント中の2104を2014に変更したというものでした。Flutterではmaterial designの2014年モデル(?)と2018年モデルに準拠したスタイルをテキストテーマとして提供しているようなのですが(正直よくわかっていない)、ドキュメントを読んでいて間違いに気づいたので修正したという感じです。

(まだstableには反映されていないので、早く反映されてほしい!)

PRを送る前の準備

PRを送る前に準備したこととしては、似たようなPRがないかを探してそれを参考にしました。それだけです(あとは、OpenなPRで似たようなものがないか探したかも)。

GitHubのPR検索から「fix typo」などで検索したところIssueを作成せずに直にPRを送っている例が見つかったので、そのPRを真似してタイトルと本文を考えました。今回の場合はコードコメントの修正のみだったので、テストが通るかどうかを確認したりスクリーンショットを添付したりする必要もなく、すんなり作成でき、次の日にはマージされました。

チャンスがあればPRを出すべき

僕は普段オープンソースにコミットすることはほぼありませんが、過去に数回だけPRを送ったことがあったのでスムーズにいきました。精神的なハードルが下がるのも大きいですが、大体どのリポジトリも手順は似ているので、一度PRがマージされるところまでいければ他でもその経験が流用できます。

こういった経験は一度すると次以降のハードルは結構下がるので、チャンスがあればぜひやってみることをおすすめします。

個人開発Flutterアプリをリリースして5ヶ月間の施策・結果まとめ

はじめに

8/1にリリースした個人開発Flutterアプリ「禁欲スカイウォーカー」ですが、1/1に5ヶ月目を迎えます。そこで、この期間にやってきたことについてまとめたいと思います。個人開発アプリの運営事情が気になるという方の参考になれば幸いです。

「禁欲スカイウォーカー」とは?

こちらの記事の冒頭をお読みください。

zenn.dev

リリース後の反響

  • 5ヶ月目にDAU700(Firebase analytics調べ)を達成、現在も維持
  • レビュー数100を達成
  • 平均4.6以上と高評価
  • Twitterのフォロワー300人以上
  • 公式LINEオープンチャット200人以上
  • 5ヶ月の収益は約7000円程度(最新月は4500円)
  • 赤字17万円

結果としてはこんな感じでした。出だしとしてはまあまあ成功と言えるのではないでしょうか(赤字17万は我ながらやばいですが)。

機能改善

この5ヶ月で追加した機能は以下のようになっています。ここでは細かい部分については触れません。

  • カレンダー
  • 目標(テキスト)
  • 称号
  • 補助カウンター
  • ランキング
  • シェア機能の改善
  • チャット(LINEオープンチャット)
  • チュートリアル(ガイド)

既存機能はこの5種類だけだったことを考えると、かなり拡張されました。とはいえ、上記の機能は基本的に類似アプリにも実装されている機能であり、機能過多には陥っていないと考えています。

  • カウンター
  • シェア
  • 目標(禁欲日数)
  • 禁欲履歴
  • 発散履歴

この5ヶ月の機能改善で工夫したこととしては、外部ツールの活用です。チャット機能は実装が難しく時間がかかるものの求めている人が多い機能でした。そこでLINEオープンチャットへのリンクだけを実装し、チャット本体はLINEを活用することで、実装を楽にしつつユーザのニーズに応えることができました。人的リソースが限られた個人開発では、使えるツールは全て使ってできるだけ楽をする ことは重要なことだと考えています。

また、チュートリアル(ガイド)・ヘルプは必ず入れたほうがいいです。意外と使い方が分からない人はいますし、 実際にチュートリアルを入れてから10%ほどインストール後最初週のリテンション率(ユーザ維持率)が上がりました。

チュートリアルは10月末に実装。それを境に10%ほどインストール後最初週のリテンション率(ユーザ維持率)が上昇) f:id:swiftfe:20201213174506p:plain

機能以外で試したこと

  • 公式Twitter
  • 広告
  • LINEオープンチャットを使ったコミュニティ形成
  • ストアのスクショ変更
  • 収益性改善

他には「ASO対策」として、検索キーワードやタイトルの調整も行いましたが、どれくらい効果が出ているかがよくわからないので今回は書かないことにします。

公式Twitter

アプリリリース日から、以下の公式Twitterを運用し始めました。

https://twitter.com/kinyoku_support

公式Twitterを作ってよかったことは以下のような点です。

  • インストール増
  • レビューを訴求することができる
  • アンケートが取れる

「インストール増」に関してですが、インストール数が少ないうちはストアの検索ランキングの下位に表示されるため、ストア外で見つけてもらう必要があります。しかしその一方で、 ある程度検索ランキングが上の方に表示されるようになると、SNS経由でのインストールは5-10%しかなくなる(つまり、App StoreGoogle Playの検索経由が90%以上) ということがわかりました。意外とユーザーはストア内の検索で見つけているようですね。

フォロワーを増やす方法

やったことはたったこれだけです。 この方法に気づいてからは非常に楽にフォロワーとインストール数を増やすことができました。

  1. 「キーワード OR @ttt -fliter:links」で検索
  2. 出てきた投稿をひたすらいいね

これで勝手にフォロワーが増えていきます。いいねするだけなので本当に簡単ですし、 こちらのフォロー数を増やさずにフォロワーだけを増やすことができます。

(アプリ広報用の)広告

iOSではApple Search Adsを、Android向けにはGoogle Adsを使いました。Google AdsよりもApple Search Adsの方がコスパが非常に高いことがわかったため、最初の一ヶ月でGoogle Adsを使うのはやめてApple Search Adsのみを使うようにしました。

また、Apple Search Adsにはbasicとadvancedの2種類のコースがあり、2つを併用して運用していました。本アプリの場合、advancedの方でbasicと同じくらいの効果を出そうとすると広告単価が高くなってしまいコストが高くなってしまうため、最終的にはbasicのみを使うことにしました。basicの最高広告単価は23円に設定しています。この単価に設定しておいても1日12,3インストールがありました。これは1日のインストール数の1/3程度にあたります。結構コスパはいいと思います。

ちなみに、5ヶ月間で使った額はというと・・17万円強です。 これは自分の勘違いもあり、思っている1.5倍ほどの金額を払ってしまっていたという問題もありましたが、それにしてもさすがに使いすぎましたね😢

また、そういった赤字の増大の懸念もあり、12月13日をもって全ての広告をとりやめました。なぜこのタイミングだったかというと、その数日前に主要キーワードの検索ランクが上がったからです。つまり、広告がなくても十分ユーザが見つけてくれるという判断です。

LINEオープンチャットを使ったコミュニティ形成

本アプリでは、アプリ内にLINEオープンチャットへのリンクを実装し、そこからオープンチャットに入れるようにしています。このオープンチャットには現在200人以上のメンバーがいます。

このチャットをつくってよかったことは主に以下の点です。

  • レビューのお願いを定期的に行うことができる (一回お願いすると2レビューくらいしてもらえる)
  • 新機能についてアンケートで調査して、実装前に需要を確認することができる
  • レビューを下げられることなく不具合の指摘をもらえる
  • チャットを気に入ってもらえるとアプリを継続して使ってもらえる
  • ユーザーの間で勝手に価値を作り出してくれる
  • 全能感が得られる

特に、レビューのお願いがいつでもできるのはかなり大きかったです。今回はアプリにチャット機能が必要だったのでLINEオープンチャットを使うことにしましたが、チャット機能が必要じゃなくてもユーザコミュニティという名目等で導入する価値は大いにあると思います。

ストアのスクショ変更

最初のアプリリリース時には、Macのプレビューで適当に背景・スクリーンショット画像・テキストを組み合わせて以下のようなスクショを作りました。

f:id:swiftfe:20201212092359p:plain

それに加えて、以下のように横画面のスクショを2種類試してみました。

インパクトの強さ+メッセージを伝えること重視 f:id:swiftfe:20201208113619p:plain

インパクトの強さ+評判を伝えること重視 f:id:swiftfe:20201208113635p:plain

横画面のスクショは大きく画面を使えるので、インパクトが強いのがメリットだと考えています。また、公式感も演出しやすいように思います。しかし、スクショを変えてもあまり目立った効果はなかったです😢

収益性改善

収益性改善の施策として、11月末から全画面広告を導入しました。これの効果は非常に高かったです。月の広告収入が2倍くらいに増えました。 ちなみに最初はバナー広告だけでした。12月の収益は約4500円で、1月も同じくらいになる予想です。

広告にはadmobを使っているのですが、3日に1回起動時に広告を出すという簡単な方法にしたため、設定・実装は数時間で終わりました。数時間で広告収入が2倍になったので、費用対効果はかなり高かったです。

(全画面広告の例)

f:id:swiftfe:20201212092311p:plain

技術記事を書いた

アプリを作った際の細かいところについての記事をいくつか書きました。

zenn.dev

qiita.com

qiita.com

など

おわりに

今回は、禁欲スカイウォーカーリリースから5ヶ月間でやったことを色々とご紹介しました。振り返ってみると、自分の試してみたいことをどんどん試すことができた5ヶ月だったと思います。 こういうところは個人開発の良い点ですね。これからも、良かった施策は継続しつつ、悪かった施策はスパッと終わらせ、より良いアプリにしていきたいと思います。

最低限のDjangoアプリ自動テスト

テストは不安を退屈に変える賢者の石だ。 ストレスを感じれば感じるほど、頻繁にテストを走らせるようになる。テストをすぐに走らせれば、ミスをしでかす確率が減っている実感が得られ、結果的にストレスが減っていく。


私たちは、完璧を求めているのではない。すべてのものをコードとテスト両方の視点から捉えることによって欠陥を減らし、 自信を持って前に進めるようになろうと考えているのだ。

ーーー「テスト駆動開発」より引用ーーー

複数人開発でテストコードがなくて困った

最近「スタマチ」の開発中、Aさんが書いたサーバーサイド(Django)のコードをBさんやCさんがフロントエンド(Flutter)から使用しようとしたとき、サーバーサイド側のコードにミスがありデバッグに非常に手間取ったということがありました。

もし、サーバーサイドでテストが書かれていたら、どうだったでしょうか?以下のことができたと思います。

  • Aさんはテストでミスを事前に発見することができた
  • BさんとCさんはテストを読むことでAPIの使い方を正しく知る事ができた

あるエンドポイントに対してのテストコードを書くことは、

  • 品質の担保
  • 手動デバッグの手間の削減
  • フロントエンドエンジニアのためのわかりやすいドキュメント

の3つの意味で重要だと思っています。

特にフロントエンドエンジニアは、

  1. エンドポイントにどのようなHTTPリクエストを送ればどのようなレスポンスが返ってくるかだけが分かればよい
  2. その内部が正しく動いているかどうかまでは気にしたくない

と考えると思います。

テストコードを書くことで、1を明示し、2を担保することができます。

DjangoDRF)で最小限のテストを書いてみる

今回はDjango REST Framework で作った APIサーバーに対するテストを書いてみます。

最小限性

  • ある一つのエンドポイントに対して
  • リクエストを送り、そのレスポンスを検証

具体的には、reportという名前のユーザ通報機能用のアプリケーションのcreateメソッド(REST FrameworkのModelViewSetを継承)に対してのテストを書きます。

views.py

from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated

from .models import Report
from .serializers import ReportSerializer


class ReportViewSet(viewsets.ModelViewSet):
    queryset = Report.objects.all()
    serializer_class = ReportSerializer
    permission_classes = [IsAuthenticated]

    def create(self, request, *args, **kwargs):
      # 省略
      return Response({'message': '通報が完了しました。'}, status=201)

tests.py

from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from .models import Report
from account.tests import *


class ReportTests(APITestCase):
    def test_create_report(self):
        reporting_user = create_user('test1', 'xxx@xxx.com', 'xxx')
        reported_user = create_user('test2', 'yyy@yyy.com', 'yyy')

        url = reverse('report-list')
        data = {'reporting_user_id': reporting_user.id,
                'reported_user_id': reported_user.id,
                'content': 'test'
                }

        self.client.login(username='test1', password='xxx')
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.data, {'message': '通報が完了しました。'})

このようにテストを書くことで、フロントエンドエンジニア側から見て、先ほど挙げた二つの要素を満たすことができます。

  1. エンドポイントにどのようなHTTPリクエストを送ればどのようなレスポンスが返ってくるかだけが分かればよい(テストを読めば分かる。)
  2. その内部が正しく動いているかどうかまでは気にしたくない(テストが通っていれば、正しく動いている。)

また、 テストが通っていることを確認しておけば、サーバーサイドエンジニアは「自信を持って」コードをフロントエンドエンジニアに渡すことができます。

テストの実行

$ python manage.py test

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.410s

OK
Destroying test database for alias 'default'...

これでテストの追加は完了です。

テストが失敗する例

これで、例えば views.py で201ではなく200を返すように書き換えた場合、以下のようにテストが失敗します。

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_create_report (report.tests.ReportTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/xxx/report/tests.py", line 40, in test_create_report
    self.assertEqual(response.status_code, status.HTTP_201_CREATED)
AssertionError: 200 != 201

----------------------------------------------------------------------
Ran 1 test in 0.411s

FAILED (failures=1)
Destroying test database for alias 'default'...

参考

DRFのテストについてのドキュメント https://www.django-rest-framework.org/api-guide/testing/

reverseに対応するurl名がわからなかったので、こちらを調べました https://www.django-rest-framework.org/api-guide/routers/#simplerouter

例えば、accounts-list で GET(list) と POST(create) の両方にあたります。これにハマりました

良いコードを書くために普段気をつけているパターン集

はじめに

この記事では、自分が普段コードを書く際に気をつけていることの中から、「良いコードを書くためのパターン」を説明していきます。今日から早速使えるようなものに絞ったつもりです。

ガード節による早期return

ガード節は、主にエラーなどの異常を受け取る可能性がある場合、 正常系の処理の手前で例外を発生させたり、returnしたりする節のこと です。一般に例外的な場合にはコードブロックが小さくなるので、先に例外を発生させたりreturnすることでその後の箇所でそれらを意識しなくて良くなり、よりコードがよみやすくなります。

StreamBuilder(stream: stream, builder: (context, snapshot){

  // ガード節
  if(snapshot.hasError){
    return Text("エラーです");
  }

  // 正常系
  if(!snapshot.hasData){
    rerurn CircularProgressIndicator(); // データが流れてくるまではインジケータを表示
  }else{
    return Text("正常に ${snapshot.data} を受け取りました");
  }
});

また、if elseとの使い分けとしては、ifもelseも正常な条件の場合はif elseを使い、例外的な条件を含む場合はガード節を使います

宣言的に書く

以下のようなListがあるとします。

List<int> collection = new List<int> { 1, 2, 3, 4, 5 };

このcollectionから、奇数だけを取り出した新しいListを作りたいとします。

命令的に書く場合、以下のようになります

List<int> results = new List<int>();
foreach(var num in collection)
{
    if (num % 2 != 0)
          results.add(num);
}

宣言的に書く場合、以下のようになります

var results = collection.where( num => num % 2 != 0);

慣れの問題もありますが、命令的に書くよりも宣言的に書いた方が実行結果がわかりやすいです。命令的に書くと、命令を追わないと結果がわからない場合が多いです。 命令的に書く場合、「命令の結果こうなる」ということになりますが、宣言的に書く場合、「こうなるようにしてほしい」という感じになります。 「結論から話した方がわかりやすい」という話と近いような、近くないような…?

クラス

できるだけprivateフィールド・メソッドを使う

クラス内からしか使われないフィールド・メソッドはprivateで宣言し、クラス外から使えないようにしましょう。これをすることで間違ってクラス外から使ってしまうことを防ぐことができますし、 クラス外から使われる可能性を考える必要がなくなり、コードがより読みやすく・デバッグしやすくなります。 以下は、広告配信のAdmobを使う際に、AndroidiOSでidを使い分けて初期化するコードです。

注:Dartでは、_(アンダースコア)をつけることで private を表します

import 'dart:io';

import 'package:firebase_admob/firebase_admob.dart';

class AdMobService{
  void init() {
    FirebaseAdMob.instance.initialize(appId: _appId());
  }

  String _appId() {
    if (Platform.isAndroid) {
      return 'ca-app-pub-yyyy';
    } else if (Platform.isIOS) {
      return 'ca-app-pub-xxxx';
    } else {
      return '';
    }
  }
}

接合部をつくる

接合部とは、「レガシーコード改善ガイド」に出てくる言葉で、ある処理の中身を変更せずに処理の振る舞いを変えられる部分のことです。例を見てもらった方がわかりやすいと思います。

接合部なし

void saveArticle(Article article){
  final api = ArticleApi();
  api.save(article);
}

接合部あり

void saveArticle(Article article, Api api){
  api.save(article);
}
saveArticle(article, ArticleApi);
// こんな感じでも使える
// saveArticle(article, testApi);

あるいは、

class Repository{
  Repository({@required this.api});
  final Api api;
 
  void saveArticle(Article article){
    api.save(article);
  }
}
final repository = Repository(api: TestApi());
repository.saveArticle(articlei);

これの嬉しい点は、 処理の中身を書き換えなくとも、引数やDIコンテナなどで渡す値を変えることで振る舞いを変えられることです。これにより、モックオブジェクトを挿入しやすくなったり、別の種類の振る舞いに切り替えたりしやすくなります。 また、処理の中身を書き換えなくて良いのでより安全です。

computed propertyの活用

クラス内に関係する処理が集中し、同じコードが色々なところに散らばることを防ぐことができます。いわゆる Don't repeat yourself に従ったコードになるわけですね。

class Item{
  int price;

  String priceWithUnit(){
    return '$price円';
  }
}

その他

できるだけreadonlyを使う

readonlyを活用することで、意図せぬタイミングで変数の値が書き換えられることを防ぐことができます。要は変更されるべきでない変数には定数 or 不変宣言をすべきということですね。

定数を直打ちせず、変数を使って名前をつける

final productionApiUrl = "https://production.com/api";
final developmentApiUrl = "http://localhost:3000/api";

定数を直打ちせず、名前をつけていると意味や用途がわかりやすくなります。

また、変数として定義しておくことで、他の場所でも同じ定数を使いたい時にタイプミスを気にせず安全に使いまわせるので便利です。

とはいえ、全ての定数を変数にしようとすると大変なので、

  • 値だけでは意味がわかりにくいもの
  • 複数の箇所で使い回すもの

を変数にすると良いと思います。

空白行で段落を作る

空白行でコードに段落を作ることで、意味の境目をわかりやすくして読みやすくなります。

Linter・Formatterを使う

Linterによる静的チェック、Formatterによる自動フォーマットでコードをより確実に綺麗な状態に保ちます。

終わりに

その他、SOLID原則やアーキテクチャも気にしていたりしますが、ここでは典型的なパターンのみを列挙してみました。 まだまだ勉強中なので、もっといろんな点に注意してコードが書けるようになっていきたいですね。

参考

個人開発アプリの公式Twitterを開設するメリット

7月下旬にリリースした個人開発アプリの公式Twitterアカウントを運用していて、メリットが大きいと思ったのでメリットについてまとめます。

箇条書きで見にくいかもしれませんが。

  • レビューをお願いすると書いてもらいやすい
    • レビューがあると次のユーザがインストールしてくれやすくなる
    • 好意で書いてくれるので、良いレビューになりやすい
  • 認知度が低くてもアプリの宣伝ができる
  • 問い合わせ先として使える
    • ユーザの満足度UP
    • 使い方がわからない人に教えてあげられるため、離脱率DOWN
    • 問い合わせを受けたことに対応→レビュー訴求 の流れはレビューしてもらいやすい!
  • フィードバックが受けられる
    • ユーザが疑問に思うところ、使いにくいと思うところなどを教えてもらえる
  • アンケートができる
    • Twitterはアンケート機能が使える
  • 資産になる
    • 別アプリを作成したときにそのTwitterアカウントで宣伝できるなど、今後にも活かせる
  • アプリの信頼性が増す
    • フォロワー数が多くなると、アカウントを見た人が信頼できるアプリだと思ってくれるようになる

Clean Architecture・DDDをもとにしたFlutterアプリのモジュール分割&命名法

DDDの戦術的設計とは、DDD(ドメイン駆動設計)という設計手法における具体的な実装の方法論を指します。

これは、レイヤー分けの方法論で、例えば以下のようになります。

  • アプリケーション固有のロジック: XXX、XXXService
  • 永続化(DBなど)に関するコード: Repository
  • アプリケーション固有のロジックを組み立てる役目: ApplicationService、UseCase
  • UI

(アプリケーション固有のロジックに関しては、Entity、ValueObjectというパターンを使うことでXXXクラスを作るという形になります)

これらは、アプリケーション全体の構成を把握するために便利でもあります。この記事では、上記をさらに拡張して、以下のようにしてみます。タイトルは「Flutterアプリの」としましたが、Flutterアプリ以外でも同じようなものかと思います。

  • ドメイン(ある分野特有の)ロジック: XXX、XXXService
  • webAPIとの通信を行う: Api、ApiClient、DAO(Data Access Object)
  • UIとUseCaseへの橋渡し役: Controller、Notifier、BLoC
  • 永続化(DBなど)に関するコード: Repository
  • ドメインロジックを組み合わせて一連の流れを作る役目: ApplicationService、UseCase
  • UI: View、Widget、Page、Dialogなど
  • DI: Provider

これに似たクリーンアーキテクチャでも、以下のような図が出てきます。

f:id:swiftfe:20200922043516j:plain
クリーンアーキテクチャ

さて、一般的な言葉だけでは分かりづらいと思いますので、実際の例を示します。Flutterの人ならよく見たことのあるCartアプリを例にします。以下のような設定です。

  • ドメインロジック: Item、Cart、CartService
  • 複数のクラスを組み合わせたロジック: Facade
  • webAPIとの通信を行う: ItemAPI、CartAPIClient
  • UIとUseCaseへの橋渡し役: CartController、CartNotifier、CartBLoC
  • 永続化(DBなど)に関するコード: CartRepository、ItemRepository
  • ドメインロジックを組み合わせて一連の流れを作る役目: CartApplicationService、ItemUseCase
  • UI: CartView、CartPage、AddToCartDialog
  • DI: CartProvider

NotifierやProviderという言葉は、Flutter開発者ならよく目にしますよね。

これを図にすると、以下のようになります。

f:id:swiftfe:20200923171352p:plain

この時点で、これらを組み合わせてアプリを作っていくことが、なんとなくイメージできるのではないでしょうか?この記事の読者の皆様のモジュール分割、命名の際の参考になれば嬉しいです。

次回「実装編」に続くかもしれません。