×

05.はじめてスマホアプリを作ってみた(開発フェーズ)【 Android / Xamarin.Forms 】

2021/03/04

目次 [隠す]

OneThird ストア画像

記事の概要

こちらの一覧の5つ目、「開発フェーズ(実際に作りはじめる)」の記事となります。

はじめてスマホアプリを作ってみた 記事一覧
  1. 検討フェーズ(どんなアプリを作るか)
  2. 要件フェーズ(どんな要件のアプリにするか)
  3. 調査フェーズ(どんな技術を使うか)
  4. 設計フェーズ(どうやって作るか)
  5. 開発フェーズ(実際に作りはじめる)
  6. 公開フェーズ(アプリを公開する)
  7. 保守フェーズ(公開から現在まで)

こちらでインストールできます
※アンドロイド版のみです。iPhoneユーザの方すみません。
Google Play で手に入れよう

全7回に分割して書いていますが長いので、ダイジェストで読みたい方はこちらの記事をご覧ください。

アラフォー初心者だけどスマホアプリを開発~リリースまでがんばってみた【Android・Xamarin.Forms】 | neputa note

この度、素人ながらスマホアプリ開発に挑戦してみました。 今回の記事では概要と経緯について書き綴ってみたいと思います。 実際に行った作業の詳細は、今後それぞれ記事を書き、こちらにリンクを追記します。作っ

blog card

はじめてのスマホアプリ開発 開発フェーズ

前回は、設計作業の工程で行ったことについて書きました。

Visual Studioで必要なプロジェクトを追加し、フォルダ構成を整え、クラスファイルを配置しながらの設計作業でした。

全体の骨組みはできているので、今回は実際にコーディングをどのように進めていったかをまとめたいと思います。

どこから着手していくか

an image of design
Photo by:Gia Oris in Unsplash

さてまずはどこからコーディングをしていけばいいでしょうか。

まずは、前回の設計作業で作ったコンポーネント図を見てみたいと思います。

Project構成図
Project構成図

この図の「Domain」の部分が「アプリの最も重要な場所」、と本に書いてありました。

今回作るのは睡眠記録を保存・閲覧するアプリです。

睡眠記録の定義、ルールなど、アプリの肝となるコードはこの「Domain」に書いていきます。

Domainは、例えスマホアプリをやめてウェブアプリを作ることになったり、データベースがRDBからドキュメントDBになったりしても変わることのない中核を担う部分です。

ということで、Domainが無ければ始まらない、ここから着手していきます。

まずは、「就寝時間」「起床時間」といった、「睡眠記録」が持つ情報です。
これらは「Value Objet」として書いていき、それぞれが持つルールなどと併せて実装しました。

そしれそれらを「Entity」として起床時間は就寝時間より未来の日時であること、などといった複合的なルールと一緒にまとめあげます。

睡眠記録のEntityはこんな感じになりました。

using System;
using OneThird.Domain.Exceptions;
using OneThird.Domain.Models.Slogs.ValueObjects;

namespace OneThird.Domain.Models.Slogs
{
    /// <summary>
    /// 睡眠記録のEntityクラス
    /// ・WakeupDateTimeはSleepDateTimeより後であること
    /// ・WakeUpdateの日付はTargetDateと同じであること
    /// ・SleepDateTimeはTargetDateの前日17時からTargetDate当日16時59分までであること
    /// </summary>
    public class SlogEntity : Entity<SlogId>
    {
        /// <summary> コンストラクタ (Create) </summary>
        /// <param name="slogId">SlogId</param>
        /// <param name="targetDate">TargetDate</param>
        /// <param name="sleepDateTime">SleepDateTime</param>
        /// <param name="wakeupDateTime">WakeupDateTime</param>
        /// <param name="rating">Rating</param>
        /// <param name="note">Note</param>
        /// <param name="userId">UserId</param>
        public SlogEntity(
            SlogId slogId,
            TargetDate targetDate,
            SleepDateTime sleepDateTime,
            WakeupDateTime wakeupDateTime,
            Rating rating,
            Note note,
            UserId userId)
        {
            Id = slogId ?? throw new ArgumentNullException(nameof(slogId));
            TargetDate = targetDate ?? throw new ArgumentNullException(nameof(targetDate));
            SleepDateTime = sleepDateTime ?? throw new ArgumentNullException(nameof(sleepDateTime));
            WakeupDateTime = wakeupDateTime ?? throw new ArgumentNullException(nameof(wakeupDateTime));
            Rating = rating ?? throw new ArgumentNullException(nameof(rating));
            Note = note ?? throw new ArgumentNullException(nameof(note));
            UserId = userId ?? throw new ArgumentNullException(nameof(userId));

            ValidatePreAndPostDates(sleepDateTime, wakeupDateTime);
            ValidateSleepDateTime(sleepDateTime);
            ValidateWakeupDateTime(wakeupDateTime);
        }

        /// <summary> Gets TargetDate </summary>
        public TargetDate TargetDate { get; }

        /// <summary> Gets SleepDateTime </summary>
        public SleepDateTime SleepDateTime { get; }

        /// <summary> Gets WakeupDateTime </summary>
        public WakeupDateTime WakeupDateTime { get; }

        /// <summary> Gets Rating </summary>
        public Rating Rating { get; }

        /// <summary> Gets Note </summary>
        public Note Note { get; }

        /// <summary> Gets UserId </summary>
        public UserId UserId { get; }

        /// <summary> 睡眠時間(TimeSpan) </summary>
        public TimeSpan SleepHours => WakeupDateTime.Value - SleepDateTime.Value;

        /// <summary>
        /// 起床日時(WakeupDateTime)の日付が対象日付(TargetDate)と同日であると
        /// </summary>
        /// <param name="target">SleepDateTime</param>
        private void ValidateWakeupDateTime(WakeupDateTime target)
        {
            if (target.Value.Date != TargetDate.Value)
            {
                throw new OutOfRangeException($"{nameof(WakeupDateTime)}:{target.Value}");
            }
        }

        /// <summary>
        /// SleepDateTime の条件 - 以下2条件を満たすこと
        ///   Condition A : TargetDateの前日17:00 ~ 当日16:59の範囲内
        /// ・Condition B : または、TargetDateの当日17:00以降の場合、
        ///                WakeupDateTimeが当日23:59以前
        /// </summary>
        /// <param name="sleepDateTime">SleepDateTime</param>
        private void ValidateSleepDateTime(SleepDateTime sleepDateTime)
        {
            if (!GetResultOfConditionA(sleepDateTime) &&
                !GetResultOfConditionB(sleepDateTime))
            {
                throw new OutOfRangeException(
                    $"{nameof(SleepDateTime)}:{sleepDateTime.Value}");
            }
        }

        // Condition A : TargetDateの前日17:00 ~ 当日16:59の範囲内
        private bool GetResultOfConditionA(SleepDateTime sleepDateTime)
        {
            var conditionStart = TargetDate.Value.Date.AddDays(-1).AddHours(17);
            var conditionEnd = TargetDate.Value.Date.Add(new TimeSpan(16, 59, 59));

            return conditionStart <= sleepDateTime.Value &&
                   sleepDateTime.Value <= conditionEnd;
        }

        // Condition B : または、TargetDateの当日17:00以降の場合、
        //              WakeupDateTimeが当日23:59以前
        private bool GetResultOfConditionB(SleepDateTime sleepDateTime)
        {
            var condition1 = TargetDate.Value.Date.AddHours(17);
            var condition2 = TargetDate.Value.Date.Add(new TimeSpan(23, 59, 59));

            return condition1 <= sleepDateTime.Value &&
                   WakeupDateTime.Value <= condition2;
        }

        /// <summary> 睡眠日時と起床日時の前後関係を検証する </summary>
        /// <param name="sleepDateTime">SleepDateTime</param>
        /// <param name="wakeupDateTime">WakeupDateTime</param>
        private void ValidatePreAndPostDates(SleepDateTime sleepDateTime, WakeupDateTime wakeupDateTime)
        {
            if (sleepDateTime.Value > wakeupDateTime.Value)
            {
                throw new PreAndPostDateException(
                    $"Sleep DateTime:{sleepDateTime.Value.ToString("yyyy/MM/dd hh:mm")}{Environment.NewLine}Wakeup DateTime:{wakeupDateTime.Value.ToString("yyyy/MM/dd hh:mm")}");
            }
        }
    }
}

「Slog」というのは、「Sleep Log」の略称として付けた名前です。
今回のアプリにおいて最も使用する頻度の高いワードなので、使いやすい短い名前を付けることにしました。

続いて、あとあとEntityを元にデータベースとやり取りする実装を「Infrastructureプロジェクト」に行うことになります。
そこで必要となるインターフェイスもこのDomainに作っておきます。

using System.Collections.Generic;
using System.Threading.Tasks;
using OneThird.Domain.Models.Slogs.ValueObjects;
using OneThird.Domain.Models.YearMonths;

namespace OneThird.Domain.Models.Slogs
{
    /// <summary> SlogRepository Interface </summary>
    public interface ISlogRepository
    {
        /// <summary> Save new Entity </summary>
        /// <param name="slogEntity">target SlogEntity</param>
        /// <returns>Task</returns>
        Task SaveAsync(SlogEntity slogEntity);

        /// <summary> Delete Entity </summary>
        /// <param name="slogEntity">target SlogEntity</param>
        /// <returns>Task</returns>
        Task DeleteAsync(SlogEntity slogEntity);

        /// <summary> Find one entity by Guid </summary>
        /// <param name="slogEntity">target SlogEntity</param>
        /// <returns>a entity</returns>
        Task<SlogEntity> FindAsync(SlogEntity slogEntity);

        /// <summary> Find all entities </summary>
        /// <param name="userId">target UserId</param>
        /// <returns>IEnumerable entities</returns>
        Task<IEnumerable<SlogEntity>> FindAllAsync(UserId userId);

        /// <summary> 特定期間を指定したクエリメソッド実装 </summary>
        /// <param name="userId">target UserId</param>
        /// <param name="fromDate">from TargetDate</param>
        /// <param name="toDate">targetto TargetDate</param>
        /// <returns>IEnumerable SlogEntities</returns>
        Task<IEnumerable<SlogEntity>> FindSpecificPeriod(UserId userId, TargetDate fromDate, TargetDate toDate);

        /// <summary> TargetDateの年と月をDistinctで取得する </summary>
        /// <param name="userId">target UserId</param>
        /// <returns>IEnumerable YearMonthEntity</returns>
        Task<IEnumerable<YearMonthEntity>> GetYearMonthOfTargetDateWithNoDuplicatesAsync(UserId userId);
    }
}

上記は完成したもので、最初はもっとスカスカです。

実装を進めていくと、後から、「こういうルールが必要だ」とか、「パフォーマンスを考慮したクエリを追加しよう」など思いついていきます。
そのたびに、このDomainを充実させていきます。

UIやDBが変わっても不変となるルールは、最終的に必要となる先々ではなく、このDomainに立ち戻ってコーディングしました。
またルールをユーザに意識させないように入力させる工夫などはUI側の責務として、Xamarin側に実装します。

こんな具合に、Infrastructure、Application、最後にXamarin(GUI)の順番でコーディングしていきました。

Infrastructureを実装している時点では、Sqliteを使うつもりでいましたが、まだいろいろ調べている途中でした。
作業を次に進められるように、とりあえず「List」を使ったメモリ上の仮DBを実装しています。

ドメイン駆動開発の実装については、こちらの本を参考にしています。
初心者にも理解しやすい説明となるよう構成されています。
「C#」でサンプルが書かれているので、わたしのようにXamarinを採用した方はより読みやすいと思います。

テスト駆動開発を参考に実装していく

an image of design
Photo by:MARSNER

実装を行っていった流れは前項の通りですが、もう一つ、「テスト駆動開発」について書きたいと思います。

ユニットテストについて

前項で、Domainから開発し、UIの実装は最後に行ったと書きました。

しかし、これでは最後までDomainやApplicationなどのプロジェクトに実装したコードが正しく動作するのか分かりません。
UIを仕上げてようやくデバッグとか恐ろしくてチビってしまいます。
また、わたしの低スペックPCではエミュレータを使ったデバッグは結構時間がかかります。

これらを解決してくれる手段として、「ユニットテスト」はとてもありがたい存在です。

最初にValue ObjetやEntityに実装したルールなども、UI無しに検証できます。
エミュレータを使わずコードを実行できるので処理時間も短く済みます。

ちなみに今回使用したのは、「xUnit」というテストフレームワークです。
xUnit.net でユニットテストを始める

また、記事にある「Chainning Assertion」というプラグインも大変便利です。

ユニットテストを書いておくことのメリット

ユニットテストを書いておくメリットは他にもあります。

初心者のわたしにとって一発でよいコードを書くことはとても難しいことです。
一度実装しても、後から修正したりすることが何度もありました。

しかし修正作業は、せっかく正しく実装した箇所を破壊してしまうリスクを伴います。

そこで、ユニットテストを書いておき、ビルドのたびに実行するようにしておくと「テストの失敗」として教えてくれます。

また、小さなメソッドを書く習慣が自然と身についていきます。
慣れないうちは、ひとつのメソッドに複数の処理を突っ込んでいき、でかいメソッドを書いてしまいがちです。

ひとつのメソッドにはひとつの目的を実装するのが良いと、あちこちで目にします。

これは、あとから修正をしようとする場合に気づきますが、複雑に複数の処理を行うメソッドは非常にやっかいです。
自分で書いたにもかかわらず、修正しようと見返したときにウンザリすること請け合いです。

単機能のメソッドを複数組み合わせて処理を行うように実装したほうがメンテナンス性は非常に高まります。
そして可読性も増します。

ではユニットテストが小さいメソッドを書く習慣にどう作用するのでしょうか。

巨大なメソッドのユニットテストは正直書く気が失せることがポイントです。
おのずとシンプルなメソッドを書き、テストも短く済む、相互に作用するが狙いです。

テストファーストによるメリット

実装し、テストを書いて検証する、これが普通だと考えていました。
だがしかし、この世界には「テスト駆動開発」略して「TDD」なるものが存在することを知ります。

テスト駆動開発(TDD)とは - IT用語辞典 e-Words検索

テスト駆動開発【TDD / Test-Driven Development / テストドリブン開発】とは、ソフトウェア開発の手法の一つで、プログラム本体より先にテストコードを書き、そのテストに通るよう

blogCardImg

「テストを先に書くとか正気か??」と最初は思いました。
ですが、これには大きな恩恵があります。

この「最初にテストを書く」ことは、つまり「実装するコードはどのような条件を満たすのかを最初に考えること」につながります。
つまり、最初にテストコードを書きながら、詳細な実装コードの設計を行うのですね。
そして、テストをパスするように実装をしていきます。
結果として、スッキリとしたコードを書く手助けとなるのです。

慣れるまではかなりたいへんです。
ですが、慣れてしまえばこっちのものです。

テストメソッドをまず考えます。
テストメソッドの名前を考えている段階で、すでに詳細設計が前進しています。
テストコードを書きながら、実装コードが満たすべきことがクリアになっていきます。

「テスト駆動開発」については、以下の本を参考にしています。
ここまで書いたのはこの本の受け売りです。
実際に、どういったことを考えながらテストを書き、実装し、また練り直していくかをコードを交えながら説明してくれているので非常にわかりやすかったです。

個人開発は、ミスを指摘してくれる人もいないので自分で自分のコードを保全する必要があります。
また自分のコードを洗練させていく作業も自分自身です。

これらの恩恵を同時に受けることができるので、わたしにとって、とってもありがたい開発手法だと感じています。

調べる力がモノを言う

an image of design
Photo by:Markus Winkler in Unsplash

設計を考えている段階でも調べる作業はとても重要でしたが、実装段階ではより困難さが増すと実感しています。

初心者にとって行き詰った際に、基本的な情報をゲットして応用するような器用な真似はまだ厳しいものがあります。
できることならば、ピンポイントで正解が書かれている情報にありつきたいものです。

ネイティブ開発を行っているのであればだいぶ違うのかもしれませんが、今回は「Xamarin.Forms」という、開発人口がそれほど多くないフレームワークを使っています。
開発人口が少なければ、転がっている情報も比例して少数です。

例えば、今回、睡眠を評価するレーティングオブジェクトを実装したいと考えました。
これはXamarinには無いのですよね。
あるもので代替することもできます。1~5の数字を選ぶセレクタとか。
だけど、どうしても5段階の星を選ぶようにしたかったのです。
絶対に。

ではどうやってこの問題を解決すれば良いでしょう。

ひとつは、「金の力にモノを言わせて有料のコンポーネントを買う」。
わたしの経済力を舐めないでほしい。無理です。却下です。

二つ目は、RatingBarが使える「ネイティブ開発に切り替える」。
これは正直何度も頭をよぎりましたが、却下としました。

三っつ目は、「情報量を増やす」、です。
つまり、日本語検索では見つからないので、英語で検索します。

結果、複数の選択肢を得ることができ、目的通りの実装を行うことができました。

情報量が多い言語やフレームワークでも変わらないと思うのですが、調べる作業は英語で行った方が絶対に早く目的にたどり着けるように思います。
日本固有の習慣に基づくような実装であれば別ですが、世界共通のコトであれば、最も話者が多い言語を使うことは大きなアドバンテージだと思います。

いまは高機能な翻訳ツールもあります。
英語といっても、「プログラミングを理解するための英語力」だけ身につければいいので気長に取り組めばそこまで難しいものではないと思います。
もし必要と感じた方はぜひ会得することをお勧めします。
きっと問題解決の大きな力になると思います。

ソース管理ツールを活用する(Git)

an image of design

コードを書いていくと、「やっぱり元に戻したい」という状況にしばしば直面すると思います。
その対象は、ひとつのソースファイルだけの場合もあれば、プロジェクト全体だったり。

もし、いつでも元に戻せるとなったら実験的にいろいろな実装を試してみたりすることもやりやすくなります。

ほとんどの開発ツールと同様に、Visual Studioでも「Git」を使用する機能が組み込まれています。

サル先生のGit入門〜バージョン管理を使いこなそう〜【プロジェクト管理ツールBacklog】

ようこそ、サル先生のGit入門へ。Gitをつかってバージョン管理ができるようになるために一緒に勉強していきましょう!

blogCardImg

Visual Studio での Git エクスペリエンス | Microsoft Docs

Visual Studio 2019 での新しい統合 Git エクスペリエンスが、生産性の向上にどのように役立つかについて学習します。

blogCardImg

ソースファイルをGitで管理すると、特定のファイルだけ元に戻したり、すべてを元に戻したりすることが容易にできるようになります。
また、まだどのように実装するか定まっていない場合、実験用のブランチを作り一通り試してからメインのブランチに実装する、といったこともできます。

初心者のわたしにとって、トライ&エラーを気軽に行えることはとっても重要なことです。
「Git」はとてもありがたいツールです。

Gitは自分のPC上でソース管理を行いますが、Webを介して使用するGithubや、Microsoftが提供するAzure DevOpsを組み合わせることで、オンライン上にリポジトリを置くことができます。
個人開発であっても、別の端末からも作業できたり、後のリリース作業においてもメリットが多くあります。
それについては次回書きたいと思います。

まとめ

an image of a conclusion
Photo by:Ann H in Pexels

今回は、実装作業をどのように進めていったかについて書きました。

もっとシンプルに進めていくこともできるでしょうし、もっと厳密にやるべきところもあると思います。

せっかく個人開発なので、自分自身が最も恩恵を受けることができる手法を見つけていくのがよいと考えています。
ポイントとして、いま現時点の自分だけでなく、後から修正を行うときの自分など、少し未来の自分も考慮した方法を選ぶことだと思います。

二度と振り返ることができないような突っ走り方だときっと後悔します。
ただ、やり過ぎると開発自体が楽しくなくなってしまいます。

参考になったかはわかりませんが、自分なりの開発方法を見つけ出す一助になれば幸いです。

長文にお付合いいただきありがとうございます。

次回は、なんだかんだで一番面倒だったリリース作業について書きたいと思います。

はじめてスマホアプリを作ってみた 記事一覧
  1. 検討フェーズ(どんなアプリを作るか)
  2. 要件フェーズ(どんな要件のアプリにするか)
  3. 調査フェーズ(どんな技術を使うか)
  4. 設計フェーズ(どうやって作るか)
  5. 開発フェーズ(実際に作りはじめる)
  6. 公開フェーズ(アプリを公開する)
  7. 保守フェーズ(公開から現在まで)

コメント