はじめに
今回、自己学習のためにKotlinとVueを使用してトレーニングアプリの実装を行いました。
消費したいカロリーと鍛えたい部位を入力するとそれに合ったトレーニング内容を提案するアプリとなっています。
以下は以前作成した設計の記事です。
https://qiita.com/writeqiita/items/9eccb4adcd2b675f0b07
コード
今回の実装のコードは以下となります。
https://github.com/writeqiita/TrainingApplication
トレーニング提案アプリの概要
主な機能
- 消費したいカロリー数、鍛えたい部位に応じてトレーニングを提案する
- 提案されたトレーニングについて、実施したか記録することができる
- 過去に実施したトレーニングの履歴、カロリーの集計を表示する
作成した画面
- ログイン画面
- 新規ユーザー登録画面
- ホーム画面
- トレーニング提案入力画面
- トレーニング提案結果画面
- トレーニング編集画面
- トレーニング履歴画面
- 新規トレーニング追加画面
- ユーザー情報更新画面
作成したデータベース
- ユーザーテーブル
- トレーニングテーブル
- トレーニング履歴テーブル
- ユーザー体重履歴テーブル
使用した技術要素
バックエンド
Kotlin(フレームワークはSpring Boot)を使用しました。
選択した理由としては、自身のプロジェクトで使用されているもので、より理解を深めたいと考えたためです。
フロントエンド
Vue.jsを使用しました。
選択した理由としては、他のプロジェクトでよく使用されていると聞きますが、私自身は実際に触ったことがなかったため使ってみたいと思ったからです。
以下は今回Vueで使用した機能の一部についてです。
パッケージ構成
バックエンド
主に以下のように役割を分けて作成しました。
- controller : フロントからのAPIリクエストを受け取り、レスポンスを返す
- service : カロリー計算などのロジックを実行する
- repository : DB操作を行う
- entity : DB操作時に使用するオブジェクト
- dto : API操作時に使用するオブジェクト
フロントエンド
今回は各画面について一つのVueファイルを作成しました。
これまで、HTML・CSS・JavaScriptはそれぞれ別のファイルに分けて作成していました。
しかし、Vueでは1つのファイルの中にこれらをまとめて記述するのが基本です。
この形式を 単一ファイルコンポーネント(SFC: Single File Component)と呼びます。
SFCでは、1つの vueファイルの中に以下の3つのブロックを記述します。
- template : 画面の構造(HTML部分)
- script : データや処理のロジック(JavaScript部分)
- style : 見た目のデザイン(CSS部分)
SFCには以下のようなメリットがあります。
- 1つのファイルにまとまっているため見やすい
- どこを修正すればいいか分かりやすい
- コンポーネントごとに独立しているため、チーム開発がしやすい
各処理の実装について
画面遷移
今回、各画面遷移のために、Vue Routerを使用しました。
Vue Routerというのは、Vueの画面遷移を行うためのライブラリーです。
これを使用することにより、ページ全体を読み込まずに、コンポーネントを切り替えるだけで画面の遷移ができるため、よりスムーズに画面遷移することができます。
また、ルーティング処理を一つのファイルにまとめ作成するため、アプリ全体の画面構成の管理をしやすくなるメリットもあります。
例: ログイン画面から新規ユーザー登録画面への遷移
- main.jsでrouterを使用するように設定する
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
2. index.jsでルーティングを設定する
const routes = [
{
// ログイン画面
path: '/',
name: 'Login',
component: LoginPage
},
{
// 新規ユーザー登録画面
path: '/register',
name: 'Register',
component: CreateUserPage
},
3. ログインページで、新規ユーザー登録画面に遷移できる関数を作成する
// 新規ユーザー登録画面へ遷移
const goToRegister = () => {
router.push('/register')
}
4. 画面遷移したい際には、3で作成した関数を呼び出す
新規ユーザー登録画面への遷移の場合、ボタンを押下したら関数が呼び出され、新規ユーザー登録画面に遷移する
<!-- 新規ユーザー登録ボタン -->
<button type="button" class="btn-register" @click="goToRegister">新規ユーザー登録</button>
API通信
今回各画面のAPI通信には、Axiosを使用しました。
Axiosとは、バックエンドとAPI通信を行うためのHTTPクライアントライブラリです。
これを使用することで、リクエストやレスポンスの送受信を簡単に記述でき、APIとのデータ通信を効率的に行うことができます。
例:ログイン処理
この処理の場合、バックエンド側の「api/auth/login」のAPIを実行するようにPOSTリクエストを行う
const handleLogin = async () => {
try {
const res = await axios.post('/api/auth/login', {
username: username.value,
password: password.value
})
if (res.data.success) {
// ユーザー情報をセッションに保存
sessionStorage.setItem('userId', res.data.userId)
sessionStorage.setItem('username', res.data.username)
sessionStorage.setItem('weight', res.data.weight)
sessionStorage.setItem('admin', res.data.admin)
// ホーム画面に遷移
router.push('/home')
} else {
errorMessage.value = res.data.message
}
} catch (err) {
errorMessage.value = 'ユーザーIDまたはパスワードが間違っています'
}
}
トレーニング提案機能
今回実装するのに一番苦戦した機能になります。(オリジナルであるため)
画面で入力された消費したいカロリー、鍛えたい部位からトレーニングを提案するという、今回のアプリのメインの機能です。
処理の大まかな流れは以下となります。
1.鍛えたい部位に合ったトレーニングをトレーニングテーブルから取得し、そこからランダムで5件選択する
// 部位でトレーニングを絞り込み
val availableTrainings =
if (parts.isEmpty()) trainingRepository.findAll()
else {
val partNumbers = parts.map { partToNumber(it) }
trainingRepository.findByTrainingPartIn(partNumbers)
}
if (availableTrainings.isEmpty()) {
logger.warn("指定部位でトレーニングが見つかりません: $parts")
return emptyList()
}
// 最大5種目をランダムに選択
val selectedTrainings = availableTrainings.shuffled().take(5)
2.消費したいカロリーを取得したトレーニング数で割り、一種目あたりの目標消費カロリーを計算する
(例:消費したいカロリーが50kcalで、取得したトレーニング数が5件の場合、1種目あたり10kcalを目標消費カロリーとする)
// カロリーを均等に分配
val caloriesPerTraining = targetCalories.toDouble() / selectedTrainings.size
3.各種目の1回あたりの消費カロリーを計算する
※ metsでの消費カロリーの計算は回数単位ではなく時間単位で計算するため、1分間に消費されるカロリーを、1分間で実施する回数で割り各種目の一回あたりの消費カロリーを計算する
(これらの計算に使用する値は全てトレーニングテーブルに登録されている)
例 : 腕立ての1分間の消費カロリーが10kcalで、腕立ての1分間あたりの実施回数が10回だった場合、腕立て1回は1kcalとなる
private fun calculateCaloriesPerRep(mets: Float, weightKg: Double, pacePerMinute: Int): Double {
val caloriesPerMinute = mets * (1.0 / 60.0) * weightKg
return caloriesPerMinute / pacePerMinute
}
4.種目ごとの目標消費カロリーを超える回数を計算する
必要な回数が少数の場合切り上げ処理を行い、確実に目標消費カロリーを超えるようにする。
例: 2.2回の場合、必要な回数は3回とする
val reps = ceil(targetCal / calPerRep).toInt()
5.各種目の回数、一回あたりの消費カロリーから、実際の消費カロリーを計算する
val actualCalories = reps * calPerRep
各画面について
ログイン画面
ログインするための画面です。
ユーザー名、パスワードを入力し、ログインボタンを押下するとログインでき、ホーム画面に遷移する機能を持っています。
新規ユーザー登録画面
ユーザーを新しく登録するための画面です。
ユーザー名、パスワードを入力し、登録ボタンを押下するとユーザーを登録でき、ログイン画面に遷移します。
ホーム画面
当日に行うトレーニングを確認したり、各画面に遷移することのできる画面です。
また、実施したトレーニングを実施済にしたり、実施したトレーニングの合計消費カロリーを表示します。
※当日のトレーニングが登録されていない場合
トレーニング提案入力画面
鍛えたい部位、消費したいカロリーを入力してトレーニング作成ボタンを押下すると、トレーニング提案結果画面に遷移し、条件にあったトレーニングを表示します。
トレーニング提案結果画面
前のトレーニング提案入力画面で入力された内容から、トレーニングを組み合わせ表示する機能となっています。
トレーニング編集画面
ホーム画面に表示されている各トレーニングの編集ボタンを押下すると、画面に遷移します。
トレーニングを変更したり、実施回数を変更することができます。
トレーニング履歴画面
これまでに行ってきたトレーニングを日付ごとに見ることができる画面です。
消費カロリーの修正も見ることができます。
新規トレーニング追加画面
新しくトレーニングを追加できる画面です。
こちらの画面は、権限を持っているユーザーのみアクセスできるように制御しています。
ユーザー情報更新画面
現在の体重を登録したり、過去の体重を見ることのできる機能となっています。
ここで入力された体重により、消費カロリーの計算に影響が出ます。
設計書について
今回、設計書を作成してから実装を行いました。設計書から作成した際の良かった点と改善点があったため記載します。
良かった点
- 設計書から作成したことにより、実際の業務に近い形で実装を行うことができた
- 作成するものがすでにわかっているため、実装中に手が止まることはほとんどなかった(エラーには結構悩まされました…)
改善点
- 画面の要素として、足りていなかった箇所がいくつもあった
- バリデーション処理についての記載がほとんどなかったため、実装時に曖昧となってしまった
- 設計には画面と大まかな機能についてしか書かれておらず、APIの仕様などについて詳しく記載されていなかった
- ユーザー情報更新画面についてはそもそも設計書が作成されていなかった
実装時に苦戦した点
今回特に苦戦した点は、トレーニング提案機能です。
トレーニング提案機能を最初に実装した際、各トレーニングの消費カロリーの合計値が、消費したいカロリーを超えないことがあり、どのようにロジックを組めばいいのか悩みました。
実装時に工夫した点
実際に自分が使うとしたら、どのような機能があったら嬉しいかを考えました。
実施したトレーニングの合計消費カロリーなどは、ホーム画面では表示するつもりはなかったのですが、実際に画面を作ってみると自分のトレーニング実行した結果がすぐに反映された方がモチベーション上がるだろうなと思い作成しました。
最後に
今回トレーニングアプリを作成するにあたって、設計の段階でもう少し詳しく書けていれば、実装の時により迷わずに作業することができたかなと思いました。
ただ、今回の実装を通してよりKotlinについて詳しくなり、Vueについてもまだ理解は甘いですが触れることはできたため、技術面で成長できました。
来月は私の苦手なテストの作成を行うため、気合いを入れて頑張りたいと思います。









