Giter VIP home page Giter VIP logo

android-practice's Introduction

Android研修

業務に近いかたちでアプリ開発を行いながら、 Androidアプリ開発の基礎復習・実務スキルを身に付けるための研修です.

このAndroidプロジェクトはandroid-training-templateからセットアップされました.
課題の詳細や進め方は元のリポジトリのREADMEを参照してください.

android-practice's People

Contributors

darmadevzone avatar seo-4d696b75 avatar actions-user avatar ykws avatar

Stargazers

daichi-matsumoto avatar

Watchers

 avatar  avatar

android-practice's Issues

JSONで天気を取得する

📄 JSON形式で天気を取得しましょう

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
    #22

課題内容

  • データモデルを定義する
  • kotlin.serializationでJSONをデコードする
  • 天気情報の取得をJSON形式のAPIに置き換える
  • 取得した天気・温度・地名を画面に表示する

利用するAPI

YumemiWeather

   suspend fun fetchJsonWeatherAsync(json: String) : String
  • ランダムにエラーが発生してUnknownExceptionをthrowします
  • ランダムな天気情報をJSON形式の文字列で返します

Parameter

Json文字列

Key フォーマット
area String 任意 東京
date String ISO8601拡張形式 "yyyy-MM-dd'T'HH:mm" 2020-04-01T12:00

Returns

Json文字列

Key フォーマット
weather String sunny, cloudy, rainy, snow sunny
maxTemp Int -- 20
minTemp Int -- -20
date String ISO8601拡張形式 "yyyy-MM-dd'T'HH:mm" 2020-04-01T12:00
area String requestと同じ 東京

動作イメージ

参考資料

Interceptorの追加

OkHttpのInterceptorを活用しましょう

Note

Required(先に完了させましょう)

課題内容

  • 通信ログを出力するInterceptorの追加
  • API呼び出しの共通クエリをInterceptorで付与する

Retrofitで実装したAPI呼び出しでは、内部的にOkHttpライブラリを利用してHTTP通信を実装しています(Square Open Sourceという同じ開発元のライブラリです)。OkHttpのInterceptorを利用すると通信リクエストの発生・レスポンスを受け取り・エラーの発生など様々なタイミングに自由な処理を挟むことができます。

Tip

Application / Network Interceptorの使い分けを意識してみましょう

ログ出力

HttpLoggingInterceptorを利用して通信ログをLogcatで見てみます🔍

image

クエリパラメータの付与

APIリクエストにはAPI key appid, 言語指定lang, 単位指定unitsと共通のクエリを追加しています。Interceptorでリクエストに一括でクエリを付与すれば各エンドポイントで個別に指定せず済みます 😎

参考資料

Composeでメイン画面を作成する

🖥️ Composeでメイン画面を追加しましょう

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
    #11

課題内容

  • Composeでメイン画面を組み立てる

画像やテキストは空もしくは適当なダミー画像・文字列で大丈夫です

UIレイアウトの構築

ComposeではColumn, Row, Boxといった標準コンポーネントを組み合わせて所望のレイアウトを組んでいきます。Viewとは異なり、Composeは何重にもネストしても計算コストの高騰を気に掛ける必要がありません!Viewでよく利用されるConstraintLayoutに相当するコンポーネントもComposeで用意されているので、好みの方法でUIを作成しましょう

メイン画面のデザイン

詳細なデザインはこちらのFigmaを参照してください

以下の条件のような画面をComposeで作成しましょう
(説明の画像はViewのものを利用しています)

  • Imageの幅は画面全体幅の半分
  • 2つのTextの幅はImageの半分

Layout

  • Imageの高さと幅は同じ
  • ImageとTextの隙間はあけない

  • Imageの水平**は画面の**と同じ
  • ImageとText合わせた矩形の垂直**は画面の**と同じ

  • ButtonとTextの隙間は80dp
  • ButtonとTextの水平**は同じ

参考資料

ViewBinding

🔗 ViewBindingを利用してXMLレイアウトをKotlinコードから参照しましょう

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 任意課題
    #8

課題内容

  • ViewBindingのセットアップ
  • findViewByIdの撤廃

XMLレイアウトをからViewを参照する場合はfindViewByIdを使用していましたが、いくつか問題があります

  • 指定したidのViewが存在しないとnullになり、NullPointerExceptionが発生する
  • 指定したViewの型と異なるとClassCastExceptionが発生する

加えてこれらの例外は実行時例外としてthrowされるため、コンパイル段階で気づくことが難しいです。ViewBindingの利用で問題を解決しましょう 🚀

参考資料

Repositoryの追加

💾 Repositoryを追加しましょう

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 任意課題
    #20

課題内容

  • Repositoryを追加する
  • 天気状態の保持&更新をViewModelからRepositoryに移行する
  • 必要な依存をViewModel, RepositoryにHiltでDIする

Repositoryとアーキテクチャ

Androidアプリ開発におけるアーキテクチャ設計はいくつかパターンがありますが、UI層とデータ層を分離する考え方には共通点があります。データ層に位置するRepositoryはUI層へデータを公開すると同時に、具体的なデータソース(APIやローカルDB)を隠蔽します。

これまでの課題でもActivityやFragmentからデータの保持と処理をViewModelへ分離してきましたが、さらにRepositoryへ分離することでUI層とデータ層を明確に区別します。

image
Android developersより引用

HiltによるDIのスコープ

スコープを適切に設定することで同一のインスタンスを注入することができます。この課題ではアプリの状態を保持&更新する役割がViewModelからRepositoryに移動していますので、RepositoryをシングルトンとしてDIすればActivityやViewModelが破棄・再生成されても状態を保持できます。

Tip

ViewModelでSavedStateHandleを利用しなくても「Don't keep activities」オプションONで状態を保持できます

参考資料

Compose Previewを利用する

🚀 Previewを利用して開発速度を上げましょう

Note

Required(先に完了させましょう)

課題内容

  • 画面全体のPreviewを追加する
  • 天気アイコン4種類を表示するPreviewを追加する

AndroidStudioにはCompose開発を助ける様々な機能がありますが、今回はPreview機能を活用していきます。Previewにより毎回アプリをEmulatorで起動しなくてもUIの外見を即座に確認できて便利です。

参考資料

エラーハンドリングとダイアログ表示(View)

💬 APIのエラーを補足してダイアログを表示しましょう。

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
  • 任意課題

課題内容

  • APIからExceptionがThrowされたらダイアログを表示する。
    • タイトル:"Error"
    • メッセージ:"エラーが発生しました。"
    • Positiveボタン:"Reload"
    • Negativeボタン:"Close"
  • ダイアログのRelaodボタンをタップすると、ダイアログを閉じ天気予報を再取得する。
  • ダイアログのCloseボタンをタップすると、ダイアログを閉じ天気予報を再取得しない。

利用するAPI

YumemiWeather

    fun fetchThrowsWeather() : String
  • ランダムにエラーが発生してUnknownExceptionをthrowします
  • 天気を表す文字列 "sunny" or "cloudy" or "rainy" or "snow"をランダムに返します

動作イメージ

参考資料

UseCaseの追加

📦 UseCaseを追加します

Note

Required(先に完了させましょう)

課題内容

  • 天気状態を更新するUseCaseを追加
  • ViewModelからはUseCaseを呼び出す

ドメイン層

Repositoryの追加ではUI層とデータ層の分離を明確化しましたが、場合によっては中間にドメイン層を設けます。ドメイン層に置かれるUseCaseは、複雑なビジネスロジックをカプセル化してViewModel(UI層)から分離したり、複数のViewModelで再利用されたりします。今回はRepositoryの関数を呼び出すだけの簡単な処理ですが、UseCaseの利用を簡単に体験してみましょう。

Tip

UseCaseは通常、ひとつの関数のみ外部に公開します(invoke()をoverrideする場合が多いです)

image
Android developersより引用

参考資料

Viewでメイン画面を作成する

🖥️ Fragmentでメイン画面を追加しましょう

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
    #5

課題内容

  • Fragmentの依存関係をapp/build.gradleに追加
  • XMLでメイン画面を組み立てる
  • メイン画面をFragmentで表示する

Fragment

mainブランチのappを実行すると"Hello World!"と表示されますが、文字を表示するViewはMainActivityのレイアウトファイルactivity_main.xmlに直接書かれています。しかし単一Activityのアプリでは往々にしてActivityが肥大化しがちです😰

そこでUIの機能単位ごとにViewをまとめてFragmentとして扱うと、Activityのコードやレイアウトファイルが簡潔になるだけでなく、画面の切り替えやUIの再利用が容易になります👍

UIレイアウトの構築

レイアウトを構成するViewGroupは様々ありますが、何重にもネストしたViewGroupは計算コストが高くなる傾向にあります。一方でConstraintLayoutは自由度が高く、1層で多くのViewGroupを重ねたようにレイアウトすることが可能です。ただし、ConstraintLayoutはそれ自身が既に計算コストが高くなる傾向にあります 🤔

適宜、どのViewGroupを選択するか十分に検討するのが良いでしょう。

メイン画面のデザイン

詳細なデザインはこちらのFigmaを参照してください

以下の条件のレイアウトファイルをConstraintLayoutで作ってみましょう(画像やテキストは空もしくは適当なダミー画像・文字列で大丈夫です)

  • ImageViewの幅は画面全体幅の半分
  • 2つのTextViewの幅はImageViewの半分

Layout

  • ImageViewの高さと幅は同じ
  • ImageViewとTextViewの隙間はあけない

  • ImageViewの水平**は画面の**と同じ
  • ImageViewとTextViewを合わせた矩形の垂直**は画面の**と同じ

  • ButtonとTextViewの隙間は80dp
  • ButtonとTextViewの水平**は同じ

参考資料

JSONデコードのUnitTest

🔍 JSONのデコード処理をテストします

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
    #25

課題内容

  • JSONをデコードするUnitTestを追加する
  • デコードに成功・失敗する場合のテストケースを用意する
  • CI(GitHub Actionsのワークフロー)でテストが自動実行される

ソースコードの品質を保つためにはテストが重要です!この課題ではJSONのデコードを例に簡単なテストを書きます。

Tip

テスト対象の入力となるJSON形式の文字列を何らかの方法で用意しましょう

参考資料

Composeで天気を表示する

🌤️ 天気を取得してメイン画面に表示しましょう

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
    #12
  • 任意課題
    #13

課題内容

Reloadボタンをタップしたら画面を更新する実装を行います

  • APIを利用して天気を取得する
  • 取得した天気を画面の天気アイコンに反映させる
  • 天気画面のComposableを分割する
  • 各Composableをstatelessにする

利用するAPI

apiモジュールのYumemiWeatherを利用します

    fun fetchSimpleWeather() : String

天気を表す文字列 "sunny" or "cloudy" or "rainy" or "snow"を返します
mainブランチの段階ではネットワーク上での通信はせずランダムな値を返します)

天気画像

こちらのSVGを利用してください

sunny
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M4.069 13h-4.069v-2h4.069c-.041.328-.069.661-.069 1s.028.672.069 1zm3.034-7.312l-2.881-2.881-1.414 1.414 2.881 2.881c.411-.529.885-1.003 1.414-1.414zm11.209 1.414l2.881-2.881-1.414-1.414-2.881 2.881c.528.411 1.002.886 1.414 1.414zm-6.312-3.102c.339 0 .672.028 1 .069v-4.069h-2v4.069c.328-.041.661-.069 1-.069zm0 16c-.339 0-.672-.028-1-.069v4.069h2v-4.069c-.328.041-.661.069-1 .069zm7.931-9c.041.328.069.661.069 1s-.028.672-.069 1h4.069v-2h-4.069zm-3.033 7.312l2.88 2.88 1.415-1.414-2.88-2.88c-.412.528-.886 1.002-1.415 1.414zm-11.21-1.415l-2.88 2.88 1.414 1.414 2.88-2.88c-.528-.411-1.003-.885-1.414-1.414zm6.312-10.897c-3.314 0-6 2.686-6 6s2.686 6 6 6 6-2.686 6-6-2.686-6-6-6z"/></svg>
cloudy
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 3c-4.006 0-7.267 3.141-7.479 7.092-2.57.463-4.521 2.706-4.521 5.408 0 3.037 2.463 5.5 5.5 5.5h13c3.037 0 5.5-2.463 5.5-5.5 0-2.702-1.951-4.945-4.521-5.408-.212-3.951-3.473-7.092-7.479-7.092z"/></svg>
rainy
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M13 2.056v-1.056c0-.552-.448-1-1-1s-1 .448-1 1v1.052c-6.916.522-10.372 5.594-11 9.906 1.864-2.677 6.136-2.677 8 0 1.839-2.641 6.047-2.685 7.917 0 1.864-2.677 6.219-2.677 8.083 0-.625-4.291-4.125-9.333-11-9.902zm0 10.101v8.843c0 1.657-1.343 3-3 3s-3-1.343-3-3v-1h2v1c0 .551.449 1 1 1s1-.449 1-1v-8.866c.68-.226 1.27-.242 2 .023z"/></svg>
snow
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512"><path d="M420.313,248.953c-9.625-19.672-22.656-37.328-38.344-52.234c8.156-17.656,12.719-37.359,12.719-58.031c0-19.094-3.875-37.391-10.906-53.984c-10.531-24.922-28.094-46.047-50.219-61.016C311.438,8.75,284.656,0,256,0c-19.094,0-37.375,3.875-54,10.906C177.094,21.453,155.969,39,141,61.141c-14.938,22.109-23.688,48.891-23.688,77.547c0,20.672,4.563,40.375,12.719,58.031c-15.688,14.906-28.719,32.563-38.344,52.234c-11.844,24.219-18.5,51.516-18.5,80.234c0,25.188,5.125,49.281,14.375,71.156c13.875,32.844,37.031,60.719,66.219,80.422C182.938,500.469,218.188,512,256,512c25.188,0,49.281-5.125,71.156-14.375c32.844-13.891,60.719-37.047,80.438-66.219c19.688-29.156,31.219-64.422,31.219-102.219C438.813,300.469,432.156,273.172,420.313,248.953zM391,386.203c-11.094,26.266-29.688,48.672-53.094,64.484c-23.406,15.797-51.5,25-81.906,25.016c-20.281-0.016-39.5-4.109-57.031-11.516c-26.281-11.094-48.656-29.703-64.469-53.094c-15.813-23.406-25-51.5-25.031-81.906c0.031-23.125,5.344-44.875,14.844-64.281c9.469-19.391,23.156-36.422,39.813-49.859c7.094-5.703,8.875-15.734,4.156-23.516c-9.313-15.438-14.656-33.438-14.656-52.844c0-14.188,2.844-27.609,8.031-39.844c7.75-18.359,20.75-34.031,37.125-45.063C215.125,42.734,234.719,36.313,256,36.297c14.188,0.016,27.594,2.875,39.844,8.047c18.344,7.75,34.031,20.766,45.063,37.109c11.047,16.359,17.469,35.969,17.469,57.234c0,19.406-5.344,37.406-14.656,52.844c-4.719,7.781-2.938,17.813,4.156,23.516c16.656,13.438,30.328,30.469,39.813,49.859c9.5,19.406,14.813,41.156,14.813,64.281C402.5,349.469,398.406,368.688,391,386.203z" /><path d="M230.781,132.391c0-8.906-7.219-16.141-16.125-16.141s-16.125,7.234-16.125,16.141c0,8.922,7.219,16.141,16.125,16.141S230.781,141.313,230.781,132.391z" /><path d="M297.344,116.25c-8.906,0-16.125,7.234-16.125,16.141c0,8.922,7.219,16.141,16.125,16.141s16.125-7.219,16.125-16.141C313.469,123.484,306.25,116.25,297.344,116.25z" /></svg>

各天気アイコンの色は以下のとおり

  • sunny #FF0000
  • cloudy #888888
  • rainy #0000FF
  • snow #44EEFF

動作イメージ

Composableの粒度

画面の構成を踏まえてComposable関数を分割します。個々のComposable関数を小さくすることで、「何を表示すればいいか」という注目の範囲や責任も小さくなり設計し易くなります。複数画面で同じようなUI要素を表示したい場合などは、Composable関数を再利用することもできます。

💡 例えば図のような分割方法が考えられます

  • WeatherApp : App全体
    • WeatherInfo : 天気情報(アイコン+気温表記)
    • ActionButtons : アクションボタンのグループ

composable-example

状態ホイスティング

ところでWeatherInfoが表示する天気アイコン・気温を直書きしたら再利用できません。状態を引数として外部から受け取り、動的に表示内容を変更できるようにします。またActionButtonsでボタンがクリックされたときの処理に関しても、ActionButtons内側に直書きしては再利用できません。そこでイベントのコールバック関数を外部から引数に渡すようにします。

stateDiagram-v2
  WeatherApp --> ChildComposable: state
  ChildComposable --> WeatherApp: event
Loading

このように状態は呼び出し元の親から子へ、逆にイベントは子から親の呼び出し元へ流れるような設計パターンを状態ホイスティングと呼びます。

利点

  • ✅ 信頼できる唯一の情報源:状態の情報源は呼び出し元1箇所に限定され状態は親から子へ単方向に伝搬するため、状態の管理が容易となりバグも防げます
  • ✅ 状態の分離:Composable内部に一切の状態を持たないStatelessな設計にすると状態管理をUIから分離できます

状態とコンポジション

宣言的UIであるComposeで画面を更新するには新しい引数でComposableを呼び出します(コンポジション)。状態を更新したときコンポジションを自動でトリガーさせるため、MutableStateで状態を保持しましょう。

Tip

  • Composableが呼び出される度にMutableStateが初期化されるのを防ぐためにはrememberを利用します
  • 状態ホスティングにより唯一の情報源はActivityとなるため、状態の管理(MutableStateの定義&APIの呼び出し)はActivity直下のComposableにのみ現れるはずです

参考資料

Gradle Kotlin DSL移行

🛠️ GradleスクリプをKotlinで書き換えます

Note

Required(先に完了させましょう)

なし

Next(次に取り組みましょう)

  • 任意課題

課題内容

  • app/build.gradleをKotlin DSLで書き換える
  • build.gradleをKotlin DSLで書き換える

依存関係の解決などAndroid開発では決して無視できない Gradle. 古いプロジェクトでは Groovyで記述される場合もありますが Kotlinでも書けます!

Kotlin DSL

Kotlinで書けると型情報があるのでエディターの型推論や自動補完の恩恵を受けられます 👍
ただしGroovyとは書き方がだいぶ異なるので注意しましょう

作業手順

  1. **/build.gradleファイルの拡張子を.gradle.ktsに変更
  2. ビルド通るようにファイルの記述を修正

YumemiWeatherがあるapiモジュールのスクリプトapi/build.gradle.ktsも参考にしてください

参考資料

Flowの導入

🚰 アプリの状態をFlowで保持しましょう

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
    #16
  • 任意課題
    #18

課題内容

  • Flowの依存をapp/build.gradleに追加
  • ViewModelで保持している状態をFlowに変更
  • UI側から状態の更新を購読して画面に反映する

FlowでUI状態を公開する

ViewModelが保持している状態をFlowに置き換えます。すると外部で状態の変更を検知できるため、画面に新しい状態を反映する処理(UIロジック)が容易になります。

Tip

Flowには書き込み可能な型と不可能な型がありますので、ViewModel内部で保持するFlowの型と外部に公開する型に気をつけましょう

Flowで状態の更新を購読する

Flowでラップされた状態の更新を検知して画面に反映します

Tip

Flowをcollectする時はActivityやFragmentのライフサイクルを意識しましょう。ComposeでUIを作成した場合はcollectAsState*APIでFlowからStateに変換します。

参考資料

Viewで天気を表示する

🌤️ 天気を取得してメイン画面に表示しましょう

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
  • 任意課題

課題内容

Reloadボタンをタップしたら画面を更新する実装を行います

  • APIを利用して天気を取得する
  • 取得した天気を画面の天気アイコンに反映させる

利用するAPI

apiモジュールのYumemiWeatherを利用します

    fun fetchSimpleWeather() : String

天気を表す文字列 "sunny" or "cloudy" or "rainy" or "snow"を返します

🚧 mainブランチの段階ではネットワーク上での通信はせずランダムな値を返します

天気画像

こちらのSVGを利用してください

sunny
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M4.069 13h-4.069v-2h4.069c-.041.328-.069.661-.069 1s.028.672.069 1zm3.034-7.312l-2.881-2.881-1.414 1.414 2.881 2.881c.411-.529.885-1.003 1.414-1.414zm11.209 1.414l2.881-2.881-1.414-1.414-2.881 2.881c.528.411 1.002.886 1.414 1.414zm-6.312-3.102c.339 0 .672.028 1 .069v-4.069h-2v4.069c.328-.041.661-.069 1-.069zm0 16c-.339 0-.672-.028-1-.069v4.069h2v-4.069c-.328.041-.661.069-1 .069zm7.931-9c.041.328.069.661.069 1s-.028.672-.069 1h4.069v-2h-4.069zm-3.033 7.312l2.88 2.88 1.415-1.414-2.88-2.88c-.412.528-.886 1.002-1.415 1.414zm-11.21-1.415l-2.88 2.88 1.414 1.414 2.88-2.88c-.528-.411-1.003-.885-1.414-1.414zm6.312-10.897c-3.314 0-6 2.686-6 6s2.686 6 6 6 6-2.686 6-6-2.686-6-6-6z"/></svg>
cloudy
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 3c-4.006 0-7.267 3.141-7.479 7.092-2.57.463-4.521 2.706-4.521 5.408 0 3.037 2.463 5.5 5.5 5.5h13c3.037 0 5.5-2.463 5.5-5.5 0-2.702-1.951-4.945-4.521-5.408-.212-3.951-3.473-7.092-7.479-7.092z"/></svg>
rainy
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M13 2.056v-1.056c0-.552-.448-1-1-1s-1 .448-1 1v1.052c-6.916.522-10.372 5.594-11 9.906 1.864-2.677 6.136-2.677 8 0 1.839-2.641 6.047-2.685 7.917 0 1.864-2.677 6.219-2.677 8.083 0-.625-4.291-4.125-9.333-11-9.902zm0 10.101v8.843c0 1.657-1.343 3-3 3s-3-1.343-3-3v-1h2v1c0 .551.449 1 1 1s1-.449 1-1v-8.866c.68-.226 1.27-.242 2 .023z"/></svg>
snow
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512"><path d="M420.313,248.953c-9.625-19.672-22.656-37.328-38.344-52.234c8.156-17.656,12.719-37.359,12.719-58.031c0-19.094-3.875-37.391-10.906-53.984c-10.531-24.922-28.094-46.047-50.219-61.016C311.438,8.75,284.656,0,256,0c-19.094,0-37.375,3.875-54,10.906C177.094,21.453,155.969,39,141,61.141c-14.938,22.109-23.688,48.891-23.688,77.547c0,20.672,4.563,40.375,12.719,58.031c-15.688,14.906-28.719,32.563-38.344,52.234c-11.844,24.219-18.5,51.516-18.5,80.234c0,25.188,5.125,49.281,14.375,71.156c13.875,32.844,37.031,60.719,66.219,80.422C182.938,500.469,218.188,512,256,512c25.188,0,49.281-5.125,71.156-14.375c32.844-13.891,60.719-37.047,80.438-66.219c19.688-29.156,31.219-64.422,31.219-102.219C438.813,300.469,432.156,273.172,420.313,248.953zM391,386.203c-11.094,26.266-29.688,48.672-53.094,64.484c-23.406,15.797-51.5,25-81.906,25.016c-20.281-0.016-39.5-4.109-57.031-11.516c-26.281-11.094-48.656-29.703-64.469-53.094c-15.813-23.406-25-51.5-25.031-81.906c0.031-23.125,5.344-44.875,14.844-64.281c9.469-19.391,23.156-36.422,39.813-49.859c7.094-5.703,8.875-15.734,4.156-23.516c-9.313-15.438-14.656-33.438-14.656-52.844c0-14.188,2.844-27.609,8.031-39.844c7.75-18.359,20.75-34.031,37.125-45.063C215.125,42.734,234.719,36.313,256,36.297c14.188,0.016,27.594,2.875,39.844,8.047c18.344,7.75,34.031,20.766,45.063,37.109c11.047,16.359,17.469,35.969,17.469,57.234c0,19.406-5.344,37.406-14.656,52.844c-4.719,7.781-2.938,17.813,4.156,23.516c16.656,13.438,30.328,30.469,39.813,49.859c9.5,19.406,14.813,41.156,14.813,64.281C402.5,349.469,398.406,368.688,391,386.203z" /><path d="M230.781,132.391c0-8.906-7.219-16.141-16.125-16.141s-16.125,7.234-16.125,16.141c0,8.922,7.219,16.141,16.125,16.141S230.781,141.313,230.781,132.391z" /><path d="M297.344,116.25c-8.906,0-16.125,7.234-16.125,16.141c0,8.922,7.219,16.141,16.125,16.141s16.125-7.219,16.125-16.141C313.469,123.484,306.25,116.25,297.344,116.25z" /></svg>

各天気アイコンの色は以下のとおり

  • sunny #FF0000
  • cloudy #888888
  • rainy #0000FF
  • snow #44EEFF

動作イメージ

参考資料

ViewModelのUnitTest

🔍 ViewModelのユニットテストを追加します

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

課題内容

  • ViewModelのユニットテストを追加
  • 天気状態の更新を呼び出して成功・失敗のテストケースを追加する
  • ViewModelがFlow・LiveDataで外部に公開している状態の変化を確認できる
  • CI(GitHub Actionsのワークフロー)でテストが自動実行される

Tip

Repositoryを導入している場合、ViewModelはRepositoryの関数を呼び出すだけ&プロパティを公開するだけの実装になっているかもしれません。代わりにRepositoryをテストします。

テストを意識したアプリ設計

テストを書く前提でプログラムを設計していないと、テストはなかなか書きづらいものです。もしテストが書けなかった場合は次のようなリファクタリングをしてみましょう。

  • API呼び出しをViewModelもしくはRepositoryに移動する
  • YumemiWeatherをInterfaceで抽象化して、ViewModelもしくはRepositoryのAPI呼び出しはInterfaceに依存させる
  • ViewModelもしくはRepositoryのコンストラクタにYumemiWeatherなどの依存を渡す

モックの利用

テスト対象のViewModelもしくはRepositoryを動かすためにはAPI呼び出しの実装(YumemiWeatherなど)が必要です。しかしテスト中にネットワーク通信が発生してしまうと様々な外的要因が入ってしまい、テスト対象自体に問題が無くてもテストが失敗する可能性があります 😇

  • ネット接続環境がない
  • 通信がタイムアウトした
  • APIサーバに問題が発生した
  • API keyが利用できない

ネットワーク通信に限らず、テストでは対象以外の依存をモック(代わりのインスタンス)に差し替えることでテスト対象に関心を集中させます 😎

参考資料

ComposeのUIテスト

ComposeのUIテストを追加します

Note

Required(先に完了させましょう)

課題内容

  • ComposeのUIテストを追加する
    • 天気状態の更新に成功したら新しい状態が画面に表示されるのを確認
    • 天気状態の更新に失敗したらエラーダイアログが表示されるのを確認

アプリの画面が実際に想定通り表示されているかを自動テストします。一般にUIテストはユニットテストと比較して、影響される要素が多くテストが煩雑・不安定と敬遠されがちです。しかしComposeではシンプルなAPIを利用してUIテストを簡単に構築することができます。

ViewModelの差し替え

APIが返す天気情報がテスト対象への入力となるので、APIレスポンスをテスト側で制御する必要があります。ただしComposableからは直接APIを呼び出さず、ViewModelから呼び出すようこれまでの課題で設計してきたので工夫が必要です。

  1. YumemiWeatherなどAPI呼び出しをモックする
  2. Hiltに代わり、モックしたAPIをコンストラクタ引数にViewModelを手動でインスタンス化する
  3. FakeのViewModelをテスト対象のComposableに渡す

Tip

テストを意識したComposableの設計が大切です。以下のようにViewModelを外部から受け取れるようにします。ただしデフォルト引数を指定して、テスト以外ではHiltでDIされるViewModelを参照します

@Composable
fun MyComposable(
    modifier: Modifier = Modifier,  // デフォルト引数ありの引数はModifierを最初に書くのが通例です
    viewModel: MyViewModel = hiltViewModels(),
) {}

参考資料

ViewModelの導入

🎯 ViewModelを追加しましょう

Note

Required(先にいずれか完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
    #15

課題内容

  • ViewModelの依存をapp/build.gradleに追加
  • ViewModelを追加
  • ViewModel内部で天気情報を取得する

AAC (Android Architecture Components)のViewModelをアプリに追加しましょう。ViewModelで天気情報を取得・保持する実装を行うと様々な利点があります 🚀

ビジネスロジックの分離

アプリの状態をUIにどう反映させるかの処理をUIロジックと呼び、ActivityやFragmentとレイアウトファイル(Jetpack Composeの場合はComposable関数)が該当します。一方でデータを管理してアプリの状態を更新する処理をビジネスロジックと呼びます。これまではActivityやFragmentに直接ビジネスロジックを記述していましたが、ViewModelに分離することでActivityやFragmentはUIロジックに専念できます。

Tip

  • Activity, FragmentからViewModelを参照するにはby viewModels()イディオムが便利です
  • Composableの引数にViewModelを追加してデフォルト引数にviewModel()を指定します

ViewModelのライフサイクル

画面の回転など構成が変更されるとActivityは再生成されるため、アプリの状態(表示されている天気アイコン)が初期状態に戻ってしまい維持されません。しかしViewModelで状態を保持すると画面の回転でも状態を維持できます👍

ViewModelはActivityよりもライフサイクルが長く、画面が回転しても生存し続けるためです。

Android developersより引用

動作イメージ

rotation-frame

参考資料

モジュール分割

プロジェクトをマルチモジュール構成にします

Note

Required(先に完了させましょう)

なし

Next(次に取り組みましょう)

  • 任意課題
    #3

課題内容

  • appモジュールの一部機能を別のモジュールに分割する
  • appモジュールから新しいモジュールを利用してビルドできる

アプリが複雑で肥大化する場合、appモジュールに全部のコードを記述すると見通しが悪くなります。ここではマルチモジュール開発を簡単に体験してみます。

Tip

YumemiWeatherを定義するためapiという別モジュールがあらかじめ用意されているので参考にしてください

参考資料

非同期処理

🌤️ 非同期に天気を取得します

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
    #21

課題内容

  • 非同期な天気APIを利用する
  • 天気の取得中はProgressBarを表示する

利用するAPI

apiモジュールのYumemiWeatherを利用します

    suspend fun fetchWeatherAsync() : String
  • ランダムにエラーが発生してUnknownExceptionをthrowします
  • 天気を表す文字列 "sunny" or "cloudy" or "rainy" or "snow"をランダムに返します
  • 結果が戻るまで数秒の時間がかかります

Coroutine(コルーチン)の呼び出し

suspendな関数は同じsuspend関数から、もしくはコルーチンから呼び出せます。ViewModelでコルーチンを起動してfetchWeatherAsyncを呼び出しましょう。

Tip

ViewModelのライフサイクルに対応したコルーチンスコープviewModelScopeを利用します。ViewModelが破棄されると起動したコルーチンも自動でキャンセルされます。

動作イメージ

参考資料

Hiltの導入

🗡️ Dagger Hiltを利用してDIします

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
    #17
  • 任意課題
    #19

課題内容

  • Hiltのセットアップ
  • YumemiWeatherをViewModelにDIする
  • SavedStateHandleを利用して状態を保持する

依存の注入

アプリでは天気を取得するのにYumemiWeatherを依存として利用しています。しかしアプリが複雑になるとより多くの依存が必要となり、手動で用意するのは大変です。DI (Dependency Injection)を利用すると自動で依存を必要な場所に用意してくれるため、大規模なアプリ開発では必須のツールです。

Tip

ビジネスロジックが集約されているViewModelに依存を注入します。

@HiltViewModel
class YourViewModel @Inject constructor(
    private val weather YumemiWeather,
) : ViewModel() { }

ViewModelで状態を保持する

ViewModel導入の課題で画面を回転させても状態(天気アイコン)を保持できるように修正しました。しかしまだ対応できない場合もあります。

  1. 開発者オプションを有効にする
  2. 「Don't keep activities」オプションをONの状態にする
  3. アプリを起動したらホームボタンなどでバックグラウンドにする
  4. 再度アプリを表示する

通常ではバックグラウンドに移行したアプリも再度表示すれば、直前の状態から引き続きアプリを利用できます。しかしAndroidシステムはメモリ解放など状況に応じてバックグラウンドのActivityを破棄する場合があり、ViewModelも同時に破棄されて状態を保持できません。開発者オプションの「Don't keep activities」はこの状況を意図的に再現するのに利用します。

donot-keep-activity

この課題では「Don't keep activities」オプションがONでもアプリの状態を保持できるように改修しましょう。

Tip

ViewModelでSavedStateHandleを利用する方法があります

動作イメージ

参考資料

Viewで詳細画面を作成する

🖥️ 詳細画面を追加してメイン画面から遷移できるようにします

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 任意課題
    #28

課題内容

  • 詳細画面のFragmentを追加する
  • メイン画面の「Next」ボタン押下で詳細画面に遷移する
  • メイン画面で表示していた都市名を詳細画面でも表示する
  • 戻るボタン押下でメイン画面に戻る

詳細画面のレイアウト

以下の条件を満たす範囲で自由にレイアウトを組んでください

  • 地点名を表記する
  • 天気予報を3時間ごとにリスト表示する

Tip

リスト表示にはRecyclerViewもしくはListViewを利用します

image

リストの各要素に表示する

  • 日時
  • 天気アイコン
  • 気温

image

Fragmentの画面遷移

Fragmentを追加したり、移動するにはFragmentManagerを利用します。戻るボタンで元の画面に遷移できよう、BackStackにトランザクションを積んでおきましょう。

Fragmentに引数を渡す

Warning

Fragmentのコンストラクタに引数を渡す方法は正しく動作しない場合があります。Activity同様にFragmentもAndroidシステムによって破棄&再生成される場合がありますが、再生成時は引数なしコンストラクタが呼ばれるためデータが失われてしまいます😰

代わりにBundleを利用します

val fragment = YourFragment().apply {
    arguments = bundleOf(
         "key" to "value",
    )
}

完成イメージ

天気予報のリスト表示は空もしくはダミーデータで大丈夫です

image

参考資料

Navigation Component による画面遷移

Navigation Componentを利用します

Note

Required(先に完了させましょう)

課題内容

  • Navigationのセットアップ
  • Navigationで画面遷移を実装する
  • SafeArgsでメイン画面から詳細画面にデータを渡す

Navigation Component

これまで画面遷移(Fragmentのトランザクション)はFragmentManagerを直接利用していました。Naviigationを利用するとより効率的に安全にFragmenの画面遷移を実装できます。

  • NavGraphの定義により宣言的に画面遷移を扱える
  • 遷移先にデータを安全に渡せる

SafeArgs

Navigationにおいてデスティネーション(遷移先のFragment)には引数を設定することができます。この引数を型安全に扱うためのGradleプラグインがSafeArgsです。プラグインのセットアップ方法は公式ドキュメントを参照してください。

例えば、次のようなデスティネーションにString型の引数を定義すると、

    <fragment
        android:id="@+id/myFragment"
        android:name="jp.co.yumemi.droidtraining.ui.MyFragment"
        android:label="Forecast">
        <argument
            android:name="key"
            app:argType="string" />
    </fragment>

MyFragmentをデスティネーションとするactionにはString型の引数が必要になります

val action = SomeFragmentDirections.someAction("value")

受け取るFragment側ではnavArgsで引数を取得できます。

// MyFragmentArgsはプラグインが自動生成した引数の型です
val args: MyFragmentArgs by navArgs()
val key: String = args.key

val viewModel: MyViewModel by viewModels()

ViewModelで引数を受け取ることもできます。

@HiltViewModel
class MyViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
) : ViewModel() {

    private val args by lazy {
        MyFragmentArgs.fromSavedStateHandle(savedStateHandle)
    }
}

参考資料

Viewで天気予報を表示する

🌤️ APIから天気予報を取得して画面に表示します

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 任意課題
    #29

Important

天気予報をAPIから取得する実装は作成済みコードを利用できます
ご自身で実装する余裕がない場合は活用してください

課題内容

  • APIで天気予報を取得する
  • 天気予報の取得中はProgressBarを表示する
  • 天気予報を詳細画面に表示する
  • 天気予報の取得に失敗したらダイアログを表示する(表示項目はメイン画面と同様)
    • Relaodボタンをタップすると、ダイアログを閉じ天気予報を再取得する
    • Closeボタンをタップすると、ダイアログを閉じメイン画面に戻る

これまで学んできた知識を活用して天気予報を表示しましょう

利用するAPI

OpenWeatherMapの5 day weather forecastを利用します。指定した地点の向こう5日間の天気情報を3時間ごとに取得できます。API keyの取得、地点の指定、レスポンスの表記方法の指定などは以前の課題 #22 を参照してください。

動作イメージ

参考資料

作成済みのコードを利用する

APIから天気予報を取得する実装は作成済みコードを利用できます

template/api-weather-forecastブランチをmainまたは作業ブランチにmergeしてください

API keyの指定

OpenWeatherMapから取得したAPI keyを記載したapi/apikey.propertiesファイルを追加します
(ファイルは.gitignoreに指定されているのでGitHub上に公開されません)

api_key="your_api_key"

API Serviceの初期化

特にパラメータを指定しなければapi/apikey.propertiesで指定したAPI keyを利用します

val weather = YumemiWeather()

利用する関数

YumemiWeather

   suspend fun fetchJsonForecastAsync(json: String) : String
  • ランダムにエラーが発生してUnknownExceptionをthrowします
  • Requestで指定した都市の天気予報をJSON形式の文字列で返します
  • Requestで指定した都市が既知の都市ID一覧に含まれない場合はIllegalArgumentExceptionをthrowします
都市ID一覧
都市名 id country
札幌 2128295 JP
釧路 2129376 JP
仙台 2111149 JP
新潟 1855431 JP
東京 1850144 JP
名古屋 1856057 JP
金沢 1860243 JP
大阪 1853909 JP
広島 1862415 JP
高知 1859146 JP
福岡 1863967 JP
鹿児島 1860827 JP
那覇 1856035 JP
New York 5128581 US
London 2643743 GB

Request

WeatherRequest

Key フォーマット
area String 都市名 東京
date String ISO8601拡張形式 "yyyy-MM-dd'T'HH:mm" 2020-04-01T12:00

Response

ForecastResponse

Key フォーマット
list List<ForecastPoint>
area String requestと同じ 東京

ForecastPoint

Key フォーマット
weather String sunny, cloudy, rainy, snow sunny
temperature Int -- 20
date String ISO8601拡張形式 "yyyy-MM-dd'T'HH:mm" 2020-04-01T12:00

DataBinding

🔗 DataBindingを利用してUIにデータを反映しましょう

Note

Required(先に完了させましょう)

課題内容

  • DataBindingのセットアップ
  • APIから取得した天気をDataBindingで画面に反映させる

これまでレイアウトの定義はXMLファイルで、データを画面に反映するUIロジックはKotlinファイルに別々に書いていました。DataBindingを利用するとUIロジックもXMLファイル側にシンプルに記述できます 🚀
するとUIに関する記述はXMLファイル側に、データの操作に関する記述はKotlinファイル側にそれぞれ集約され見通しも良くなります 👍

今回は天気を表すデータをレイアウトファイルに追加します
(String型以外にもEnum Classなども考えられます)

<layout xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:app="http://schemas.android.com/apk/res-auto">
        <data>
            <variable
                name="weather"
                type="String" />
        </data>
        <ConstraintLayout... /> <!-- UI layout's root element -->
</layout>

参考資料

Composeで詳細画面を作成する

🖥️ 詳細画面を追加してメイン画面から遷移できるようにします

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 任意課題
    #31

課題内容

  • 詳細画面のComposableを追加する
  • Navigation Composeで画面遷移を実装する
    • メイン画面の「Next」ボタン押下で詳細画面に遷移する
    • 戻るボタン押下でメイン画面に戻る
  • メイン画面で表示していた都市名を詳細画面でも表示する

詳細画面のレイアウト

以下の条件を満たす範囲で自由にレイアウトを組んでください

  • 地点名を表記する
  • 天気予報を3時間ごとにリスト表示する

Tip

リスト表示(縦方向)にはLazyColumnを利用します

image

リストの各要素に表示する

  • 日時
  • 天気アイコン
  • 気温

image

完成イメージ

天気予報のリスト表示は空もしくはダミーデータで大丈夫です

image

参考資料

API接続の実装

📩 実際にネットワーク経由でAPIを利用しましょう。今回は無料でも利用できるOpenWeatherMapを使います

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
    #24
  • 任意課題
    #23

Important

この課題は選択必須です
ご自身で直接実装する代わりに作成済みのコードを利用する選択もできます

課題内容

  • OpenWeatherMapのAPI keyを取得する
  • API keyをコードとは別ファイルに保存する
  • RetrofitでCurrent Weather Data APIを呼び出す
  • 天気・最低気温・最高気温・都市名を画面に表示する

API keyの保存

API keyなど認証情報は自分以外に知られたくはありません。センシティブな情報はソースコードにハードコーディングせず、別ファイルに保存してソースコードから読み出して使いましょう。ただ認証情報を書いたファイルをそのままGitHubに公開しては意味がないので、**/.gitignoreファイルを適宜編集してGitHub上にpushされないよう注意しましょう 🚨

RetrofitでAPI呼び出し

Retrofitライブラリを利用するとinterfaceによる抽象的な定義だけで通信を簡単に実装できます 🚀
加えてConverterを指定すればAPIレスポンスの文字列からデータモデルのデコードまで処理しくれます!以前の課題で kotlin-serializationによるJSONデコードを実装したので、同じく kotlin-serializationを利用するKotlin Serialization Converterを使ってください。

動作イメージ

OpenWeatherMapの説明

API keyの取得

まずはこちらのページからアカウントを登録します

次にAPI keyを新たに作成します

アカウントを作成してログイン成功したら、画面右上より「My API keys」を押下
image

画面右側の「Create key」から新しいkeyを作成します
image

すると一覧に作成したkeyが表示されます
スクリーンショット 2023-02-14 19 34 47

利用するAPI

OpenWeatherMapでは多種のAPIが提供されていますが、今回は無料枠でも使用できる current weather dataを呼び出します

APIのパラメータ指定やレスポンスの詳細などはAPI docsを参照しましょう

Tip

地点の緯度経度lon, latでも指定できますが、意図しない都市がヒットする場合があります。代わりに都市IDidをクエリパラメータに指定してください。有効な都市IDの例を下の表に示しましたので、ランダムな都市を選んで天気情報を取得してみましょう。

都市ID一覧
都市名 id country
札幌 2128295 JP
釧路 2129376 JP
仙台 2111149 JP
新潟 1855431 JP
東京 1850144 JP
名古屋 1856057 JP
金沢 1860243 JP
大阪 1853909 JP
広島 1862415 JP
高知 1859146 JP
福岡 1863967 JP
鹿児島 1860827 JP
那覇 1856035 JP
New York 5128581 US
London 2643743 GB

都市名は日本語で、数値はメートル法でJSONを返してもらうには、クエリパラメータにlang=ja, units=metricを付与します。

APIレスポンスの読み方

前回の課題で利用したYumemiWeatherのレスポンスに対応するフィールドは、

  • weather: .weather[0].id(idと天気状態の対応はWeather ConditionのAPI docsを参照
  • maxTemp: .main.temp_max
  • minTemp: .main.temp_min
  • date: .dt(秒単位のUnix Timestamp)
  • area: .name(場合によってはローマ字表記)

参考資料

作成済みのコードを利用する

template/api-current-weatherブランチをmainまたは作業ブランチにmergeしてください

API keyの指定

OpenWeatherMapから取得したAPI keyを記載したapi/apikey.propertiesファイルを追加します
(ファイルは.gitignoreに指定されているのでGitHub上に公開されません)

api_key="your_api_key"

API Serviceの初期化

特にパラメータを指定しなければapi/apikey.propertiesで指定したAPI keyを利用します

val weather = YumemiWeather()

利用する関数

YumemiWeather

   suspend fun fetchJsonWeatherAsync(json: String) : String
  • ランダムにエラーが発生してUnknownExceptionをthrowします
  • Requestで指定した都市の天気情報をJSON形式の文字列で返します
  • Requestで指定した都市が既知の都市ID一覧に含まれない場合はIllegalArgumentExceptionをthrowします
Request, ResponseのJSON形式は前課題と同じです

Parameter

Json文字列

Key フォーマット
area String 都市名 東京
date String ISO8601拡張形式 "yyyy-MM-dd'T'HH:mm" 2020-04-01T12:00

Returns

Json文字列

Key フォーマット
weather String sunny, cloudy, rainy, snow sunny
maxTemp Int -- 20
minTemp Int -- -20
date String ISO8601拡張形式 "yyyy-MM-dd'T'HH:mm" 2020-04-01T12:00
area String requestと同じ 東京

LiveDataの導入

Note

Required(先に完了させましょう)

Warning

LiveDataはFlowに比べてレガシーなAPIです。
内定者研修として課題に取り組むなど特別な場合以外はスキップしてください。

課題内容

  • FlowをLiveDataに置き換える

LiveDataはFlow同様に外部から状態の更新を監視可能にするAPIのひとつです。昨今のアプリ開発では最新のFlowを積極的に採用する場合が多いですが、依然としてLiveDataも使われ続けています。

参考資料

Composeの導入

🛠️ Jetpack Composeを利用する環境を準備しましょう

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
    #10

Important

この課題は選択必須です
ご自身で直接実装する代わりに作成済みのコードを利用できます

課題内容

  • Composeのセットアップ
  • Composeで"Hello World!"を出力する

Jetpack ComposeはAndroidの新しいUIツールキットです。XMLファイルを利用するViewとは対照的に、宣言的にUIを定義できるためより直感的に・少ないコード量でレイアウトを組めます。

参考資料

作成済みのコードを利用する

最新の Android Studio で新規プロジェクトを作成すると、デフォルトで Compose を利用した雛形が作成されます。既存のViewで実装されたプロジェクトを Compose に移行する場合を除けば、多くの場合でこの課題の作業は不要となります。

template/composeブランチに Android Studio が自動作成する雛形と同様のコードがあるので、mainまたは作業ブランチにmergeしてください

CIの設定

コードの品質を担保するためにLint(静的コード解析ツール)や自動テストの実行を習慣づけましょう 😎
今回はGitHub Actionsを利用してテストのワークフローを実行させます.

Note

Required(先に完了させましょう)

なし

Next(次に取り組みましょう)

  • 必須課題
    • #4 (XML)
    • #9 (Compose)
  • 任意課題

Important

この課題は選択必須です
ご自身で直接実装する代わりに作成済みのコードを利用する選択もできます

課題内容

  • PR (Pull Request)を出すと自動的にGitHub Actionsが実行される
  • Android Lintを実行できる
  • Unit Testを実行できる
  • ブランチ保護ルールの設定

GitHub Actions

ワークフローの起動タイミングは色々指定できるので、PRを出した時に自動で起動するよう設定します.
もしLintやテストが失敗すればワークフローも失敗するので、PRをマージする前に異常に気付けます👍

↓完成イメージ↓ PRを出すと自動的に起動するワークフロー
image

Android Lintや単体テストの実行はAndroidStudioと同様にGradleタスクとして実行させますが、まずはワークフローでJavaを使える環境をセットアップします ☕️
GithHub Actionsのマーケケットプレイスで提供されている様々なアクションを利用すると、複雑な処理を簡単な呼び出しで実現できます!JDKのセットアップにはこちらのアクションを利用しましょう

Android Lintの実行

Gradleタスク:lintもしくはlint${build_variant}

Androidアプリ開発に関する様々なバグの元を解析して教えてくれます

$ ./gradlew lintRelease

単体テストの実行

Gradleタスク:test${build_variant}UnitTest

**/src/test/以下に定義したすべての単体テストを実行します

$ ./gradlew testReleaseUnitTest

ブランチの保護ルール

デフォルトではレビュワーの承認が無くても、GitHub Actionsで実行したLintやテストが失敗してもPRはマージできてしまいますが、こうした事故を未然に防ぐ仕組みがあります.

ルールの追加方法
  1. PR画面中の「Add rule」ボタンを押下
  1. ルールを設定します
  • Branch name pattern
    デフォルトブランチを指定します
  • ✅ Require a pull request before merging
    PRなしでデフォルトブランチにマージできないよう制限します
    加えてレビュワーの承認を1件以上受けないとマージできないよう「Require approvals」にも✅を入れて設定します
  • ✅ Require status checks to pass before merging
    Lintとテストが成功しないとマージできないよう制限します
    🔍検索バーにワークフローの名前を入力して対象を選択します(❗️実行済みのワークフローしか出てこないので注意❗️)
  1. 「Create」ボタン押下
保護ルールを設定すると...

レビュワーの承認(approve)が無いとマージできない

テストが失敗するとマージできない
(画像はdangerを利用して失敗したテストの場所をPRにコメントしています)

参考資料

作成済みのコードを利用する

  1. template/ciブランチをmainまたは作業ブランチにmergeしてください
  2. .github/actions/check-pull-request/以下のファイルすべてを.github/workflows/以下に移動してください

こちらの作成済みブランチでは課題内容に加えて以下の機能が追加されています

  • ktlintの実行
  • gradle, ruby gemのキャッシュ利用
  • 各種lint、テスト結果をdangerでPRにコメント

エラーハンドリングとダイアログ表示(Compose)

💬 APIのエラーを補足してダイアログを表示しましょう。

Note

Required(先に完了させましょう)

Next(次に取り組みましょう)

  • 必須課題
    #14
  • 任意課題
    #13

課題内容

  • APIからExceptionがThrowされたらダイアログを表示する。
    • タイトル:"Error"
    • メッセージ:"エラーが発生しました。"
    • Positiveボタン:"Reload"
    • Negativeボタン:"Close"
  • ダイアログのRelaodボタンをタップすると、ダイアログを閉じ天気予報を再取得する。
  • ダイアログのCloseボタンをタップすると、ダイアログを閉じ天気予報を再取得しない。

利用するAPI

YumemiWeather

    fun fetchThrowsWeather() : String
  • ランダムにエラーが発生してUnknownExceptionをthrowします
  • 天気を表す文字列 "sunny" or "cloudy" or "rainy" or "snow"をランダムに返します

UI状態とイベント

エラーなどのイベントをUIでどう処理するのか?ではなく、イベントによってUIが表示すべき状態をどう変化させるか、という観点でモデル化します。今回ではエラーが発生すると、エラーダイアログを表示するか・非表示かの状態に影響しますので、例えば次のようにUI状態を定義できます

data class WeatherState(
    val weather: String?, // もっと適切な表現方法があります!
    val showErrorDialog: Boolean,
)

💡 ComposeではUI状態をひとつのDataClassにまとめて扱う場合が多いです

動作イメージ

参考資料

Composeで天気予報を表示する

🌤️ APIから天気予報を取得して画面に表示します

Note

Required(先に完了させましょう)

Important

天気予報をAPIから取得する実装は作成済みコードを利用できます
ご自身で実装する余裕がない場合は活用してください

課題内容

  • APIで天気予報を取得する
  • 天気予報の取得中はProgressBarを表示する
  • 天気予報を詳細画面に表示する
  • 天気予報の取得に失敗したらダイアログを表示する(表示項目はメイン画面と同様)
    • Relaodボタンをタップすると、ダイアログを閉じ天気予報を再取得する
    • Closeボタンをタップすると、ダイアログを閉じメイン画面に戻る

これまで学んできた知識を活用して天気予報を表示しましょう

利用するAPI

OpenWeatherMapの5 day weather forecastを利用します。指定した地点の向こう5日間の天気情報を3時間ごとに取得できます。API keyの取得、地点の指定、レスポンスの表記方法の指定などは以前の課題 #20 を参照してください。

動作イメージ

参考資料

作成済みのコードを利用する

APIから天気予報を取得する実装は作成済みコードを利用できます

template/api-weather-forecastブランチをmainまたは作業ブランチにmergeしてください

API keyの指定

OpenWeatherMapから取得したAPI keyを記載したapi/apikey.propertiesファイルを追加します
(ファイルは.gitignoreに指定されているのでGitHub上に公開されません)

api_key="your_api_key"

API Serviceの初期化

特にパラメータを指定しなければapi/apikey.propertiesで指定したAPI keyを利用します

val weather = YumemiWeather()

利用する関数

YumemiWeather

   suspend fun fetchJsonForecastAsync(json: String) : String
  • ランダムにエラーが発生してUnknownExceptionをthrowします
  • Requestで指定した都市の天気予報をJSON形式の文字列で返します
  • Requestで指定した都市が既知の都市ID一覧に含まれない場合はIllegalArgumentExceptionをthrowします
都市ID一覧
都市名 id country
札幌 2128295 JP
釧路 2129376 JP
仙台 2111149 JP
新潟 1855431 JP
東京 1850144 JP
名古屋 1856057 JP
金沢 1860243 JP
大阪 1853909 JP
広島 1862415 JP
高知 1859146 JP
福岡 1863967 JP
鹿児島 1860827 JP
那覇 1856035 JP
New York 5128581 US
London 2643743 GB

Request

WeatherRequest

Key フォーマット
area String 都市名 東京
date String ISO8601拡張形式 "yyyy-MM-dd'T'HH:mm" 2020-04-01T12:00

Response

ForecastResponse

Key フォーマット
list List<ForecastPoint> -- --
area String requestと同じ 東京

ForecastPoint

Key フォーマット
weather String sunny, cloudy, rainy, snow sunny
temperature Int -- 20
date String ISO8601拡張形式 "yyyy-MM-dd'T'HH:mm" 2020-04-01T12:00

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.