Androidハンズオン
森松琢弥
はじめまして。リクルートテクノロジーズ新人の森松琢弥です!
3ヶ月間の研修を経て、現在はリクルート住まいカンパニーでSUUMOのAndroidアプリエンジニアとして働いています。
今回は Androidアプリのハンズオンとして簡単なニュースアプリを作成しつつAndroidアプリ開発の流れについて紹介したいと思います。具体的にはAPIから取得した記事情報をリスト形式で表示し、タップ時に該当ページを表示するアプリを作成しながら解説していきます。
対象読者としてはプログラミング経験はあるがwebやアプリの開発ははじめてで、これからAndroidのキャッチアップを行う方・行いたい方を想定しています。
今回のハンズオンはKotlinで実装を行っており、ソースコードはgithubは公開しています。
https://github.com/tmorimatsu/NewsApp
背景
この記事を書く背景として私は入社前に個人でAndroidアプリ開発を行っており、その時のAndroidの初学時でのキャッチアップが難しかったということがあります。
具体的には
- 情報が多く、分散していてどのように始めればいいかわからない
- 細かい説明が続きモチベーションが続かない
- 文字を表示するだけのような簡単すぎる内容でこの後どのように学習を進めていけばいいのかわからない
といったものがありました。
他の言語での開発経験が豊富な方であればそこまで難しくないのかも知れませんが、単なる大学生であった私や私の周りの友人などにとっては難しく、Android開発に興味があるものの上記のような理由から学習が進まず諦めてしまう人がたくさんいました。その中で私自身が開発を続けられた要因として、細かい理論の理解を後回しにし、最低限の作りたいものを作る方法を調べながらとりあえず動くものを作成することでキャッチアップを行ったことがあったのではないかと思っています。もちろん理論の理解がとても大事なのは言うまでもありませんが、経験上キャッチアップを行う段階ではモチベーションを下げる要因になるため、後回しにすることも有効だと思っています。
そこでこの記事ではAndroidアプリを作成するために必要最小限の、理解が必要なところ以外の説明は極力省き、動く形でまとまったアプリを作成することで、上記した3つの問題を克服するハンズオンを行います。
各論の勉強で疲弊する前にまず動くアプリを作成してみて楽しさを実感し、その後理論的な部分をキャッチアップしていくことによって最終的に開発者として働くといった人が増えると嬉しいと思っています。この記事によって、最低限の知識の理解をしつつアプリを作ることの楽しさを感じられ、次の学習へのステップとなることを狙いとしています。
作成するアプリの全体像
今回作成するアプリは以下のようなものです。
アプリ起動時にNewsAPIという無料のAPIサービスからニュースデータを取得し、リスト形式で表示、リストをタップすることで、ニュースの詳細ページへと遷移し詳細なニュースを見ることができるといったものになっています。
基礎的な内容としてここではAPI通信とリスト表示を紹介するためにこのアプリを題材としました。
現在配布されているアプリでAPI通信もリストも利用せずに作成されているアプリはおそらくほとんど無いでしょう。(ゲームは除く)
皆さんの大好きなYouTubeだってSUUMOだってアメンボだってみんなみんなAPI通信とリストにて作成されています。また、特にリストは頻繁に利用されるものであるにもかかわらず、少し複雑で、学習の初期の段階ではわかりにくい部分であると言えます。実際に以前の記事(新卒エンジニアが配属後3ヶ月間で学んだ事 ~ 開発・ユーザ・ビジネス の理解 ~)では、ある程度のWeb開発経験のある私の同期でもAndroidのキャッチアップをした際にわかりにくかった概念としてリストでの表示を挙げています。これらからもAPI通信とリストをハンズオンの題材とするのは汎用的で今後のアプリ開発にも展開しやすく妥当だと考えました。
今回実装するクラスは全部で以下のようになっています。
MainActivity.kt は今回のアプリの起動時に実行されるクラスです。実行されるとまず API通信が行われ、その後レスポンスのデータをリストで表示します。
図の上の部分がAPI通信です。ライブラリを利用して NewsAPI にリクエストを行い、レスポンスを取得します。NewsAPI のレスポンスは JSON 形式にて返却されますが、これを扱いやすくするために自身で実装したクラスへと変換して MainActivity.kt で受け取ります。
図の下の部分がリストでの表示部分で、API からのレスポンスを受け取った後にリスト形式での表示を行うための NewsAdapter を作成して MainActivity.kt でリストを表示します。
それぞれのクラスの役割については実装のタイミングで解説します。
アプリの作成順序は以下の順番で行います。
- 準備
- Android Studio のインストール
- NewsAPI の APIキーの取得
- プロジェクトの作成
- エミュレーターでの実行
- 表示内容の変更
- 実装
- ライブラリのインポート
- API通信によるデータ取得
- リスト表示
準備
Android Studio のインストール
まず Android Studio をインストールする必要があります。
詳細なインストール手順の説明は省きますので、以下のリンクを参照にしてインストールを完了させてください。
https://developer.android.com/studio/install?hl=ja
NewsAPI の APIキーの取得
以下のリンクにアクセスし、Get API Key ボタンをクリックします。
規約(terms)を読み、名前・メールアドレス・パスワード・その他のチェックボックスにチェックを入力し、submit をクリックします。
APIキーが取得できます。下の画像の黒で塗りつぶしているところに表示されている文字列が APIキーです。
ちなみに、こちらは NewsAPI により提供されていることを明記した上で非商用利用でのみしか利用できませんのでご注意ください。
取得した APIキーは後ほど利用します。
プロジェクトの作成
インストールが完了したら、プロジェクトを作成します。
Android Studio を起動して、Start a new Android Studio Project をクリックし、以下の用にfinishまで進めてください。
プロジェクトの生成が完了すると以下のようなフォルダ構成でファイルが作成されます。
エミュレーターでの実行
ここで一度自動生成されたアプリをエミュレーターで実行してみましょう。まず右上の緑の三角ボタンをクリックします。
エミュレーターを選択する画面が出てきます。Create Virtual Devices をクリックします。
端末を選択します。今回はNexus 5Xを選択し、Nextをクリックします。
利用するOSを選択します。下の画像ではもうすでにいくつかダウンロードしていますが、今回はAPI 28をダウンロードします。
しばらく待つとダウンロードが完了するのでFinishをクリックします。
AVD Name にSampleEmulatorと入力し、Finishをクリックします。
すると作成したエミュレーターがリストに出てくるのでこちらを選択しOKをクリックします。ちなみに2回目以降実行するときはここからSampleEmulatorを選択することで実行できます。
実行すると以下のようにエミュレーターが起動し、”Hello World!” が画面中心に出力されている画面が表示されます。
Android の画面は基本的には Java, Kotlin ファイルとリソースファイルで構成されており、実行されるクラスからリソースを呼び出して利用するという形になります。よくあるのがレイアウトを xml で定義し、コードから呼び出すものです。
アプリ起動時に MainActivity.kt の onCreate が実行され、その中で
1 |
setContentView(R.layout.activity_main) |
が呼び出されています。これは activity_main.xml のレイアウトを画面に反映する処理です。
activity_main はプロジェクトの作成時に自動生成されており、内容は以下です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"/> </android.support.constraint.ConstraintLayout> |
レイアウトファイルは基本的に要素を囲む Layout とその中身の View で構成されます。
ここでは ConstraintLayout で内側の TextView の配置を決定しています。
Layout とその中身の View ともに縦・横のサイズを設定する必要があり、これは
1 2 |
android:layout_width android:layout_height |
で設定されています。wrap_content を指定した場合にはこのビューの中身の大きさに合わせることになり、match_parent を指定した場合には親ビュー内の大きさになります。
表示内容としては設定されている
1 |
android:text="Hello World!" |
によって “Hello World!” が表示されます。
表示内容の変更
次に以下のように1行づつ追加してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView <!--ここから--> android:id="@+id/textview" <!--ここまでを追加--> android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"/> </android.support.constraint.ConstraintLayout> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.example.tmorimatsu.newsapp import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.widget.TextView class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // ここから findViewById<TextView>(R.id.textview).text = "News App" // ここまでを追加 } } |
xml では TextView に対して”textview”という ID を設定しており、そのIDを findViewById で取得することで、コード側から表示内容などを操作することができるようになります。ここでは表示するテキストを”News App”に変更しています。
ここでもう一度実行してみましょう。
先程 “Hello World!” だった表示が期待通り “News App” に変更されています。
ニュースアプリの実装
ライブラリのインポート
今回利用するライブラリの設定を行います。
左側のフォルダツリーから build.gradle(Module:app) を開き、dependencies の中に以下のように追記します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 28 defaultConfig { applicationId "com.example.tmorimatsu.newsapp" minSdkVersion 21 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' // ここから implementation 'com.android.support:recyclerview-v7:28.0.0' // RecyclerView (リスト用コンポーネント) implementation 'com.android.support:customtabs:28.0.0' // ChromeCustomTabs (WebViewとしてchromeを利用するタブ) implementation 'com.squareup.retrofit2:retrofit:2.3.0' // Retrofit2 (通信用ライブラリ) implementation 'com.squareup.retrofit2:converter-gson:2.3.0' // Gson (Json<->Javaクラス変換ライブラリ) implementation 'com.github.bumptech.glide:glide:4.8.0' // Glide (画像用ライブラリ) annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0' // Glide (画像用ライブラリ) // ここまでを追加 } |
記述が完了したら右上のsyncをクリックします。
ここに関しては今の段階ではこのように書くんだなとくらいに思っていただければ結構です。もし興味がある方は公式サイトのこちらもしくはこちらを読んでいただけると良いかと思います。
API通信によるデータの取得
API通信の処理を記述していきます。
ここではRetrofitというライブラリを利用してAPI通信を行います。
ここでの構成は以下のようになっています。
MainActivity.kt から ApiService.kt を利用してAPIへリクエストを送り、APIからのレスポンスデータをResponseData.kt, Article.kt に変換し利用します。
なぜ、ResponseData.kt, Article.kt が必要なのか疑問を持たれた方は多いと思います。JSONを自力でパースして利用することは可能ですが、項目が多いとしんどくなります。(今回は少ないですが)
それに対応するために、JSON を Java や Kotlin のクラスに変換してくれるコンバーターが提供されています。ここでは Gson ライブラリによって変換します。JSON のキーとクラスの変数名を合わせることで自動変換を行ってくれます。
NewsAPI からのレスポンスは上図のようなJSONです。今回は記事のリストとしてarticles, articles リスト内の1記事の内容としてurl, urlToImage, publishAt, content の4つを利用します。
JSONから変換されたこれらの値を保持するクラスとしてResponseData.kt, Article.kt を作成します。
ktファイルは app/パッケージ名.newsapp を右クリック -> Kotlin File/Class をクリックし、NameとTypeを指定して作成します。
Article.kt は
Name:Article
Kind: Class
として作成します。
同様にResponseData.ktも
Name: ResponseData
Kind: Class
にて作成し、それぞれ以下の用に実装します。
1 2 3 4 |
package com.example.tmorimatsu.newsapp // 1記事を保持(記事url, 画像url・発行日・記事内容) data class Article(val url: String, val urlToImage: String, val publishedAt: String, val content: String) |
1 2 3 |
package com.example.tmorimatsu.newsapp data class ResponseData(val articles: List<Article>) |
次にRetrofitのInterfaceの実装を行います。ApiService.ktを以下の内容で作成します。
Name: ApiService
Kind: Interface
ApiService.ktではAPIアクセスのためのメソッド・エンドポイント・クエリパラメーターを指定します。
1 2 3 4 5 6 7 8 9 10 |
package com.example.tmorimatsu.newsapp import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Query interface ApiService { @GET("/v2/top-headlines") fun getNews(@Query("apiKey") apiKey: String, @Query("country") country: String): Call<ResponseData> } |
Get メソッドにて、以下の URL を呼び、レスポンスの値を ResponseData クラスで受け取るには上記の用に記述します。
ベースURL/v2/top-headlines?apiKey={第一引数}&country={第二引数}
ここまでで API リクエストを送る準備が整ったので MainActivity でリクエストを投げる処理を実装します。取得した API キーと書いているところには準備段階で取得したNewsAPIのAPIキーを設定してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
package com.example.tmorimatsu.newsapp import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.widget.TextView import retrofit2.Call import retrofit2.Callback import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // レイアウトファイルのセット setContentView(R.layout.activity_main) // Retrofitクライアントの取得 val retrofit = Retrofit.Builder().baseUrl("https://newsapi.org/").addConverterFactory(GsonConverterFactory.create()).build() // APIエンドポイントの生成 val api = retrofit.create(ApiService::class.java) // 引数によってapiエンドポイントを指定し、リクエスト api.getNews("取得したAPIキー", "jp").enqueue(object: Callback<ResponseData> { // 通信が失敗したときの処理 override fun onFailure(call: Call<ResponseData>?, t: Throwable?) { // 今回は失敗したときは無視しています。 } // 通信が成功したときの処理 override fun onResponse(call: Call<ResponseData>?, response: Response<ResponseData>?) { // 紐づけたTextVeiwに取得したデータをそのまま表示 findViewById<TextView>(R.id.textview).text = response?.body().toString() } }) } } |
行っていることとしてはRetrofitクライアントの生成し、APIリクエストの実行、レスポンスの処理です。
Retrofit クライアントの生成では、ベース URL として”https://newsapi.org/”を、コンバーターとしてGson を設定しています。
その後、API リクエストを行っており、Callback 内で API のレスポンスが来たときの処理を記述します。名前のとおりですが、onFailure で失敗時の処理、onSuccess で成功時の処理を行います。ここでは失敗した場合を無視し、成功した場合のみ一旦取得したデータをそのまま画面に表示しています。
またAndroidアプリでは通信を行うためにパーミッションが必要で、その処理はAndroidManifest.xmlにを記述します。このパーミッションを書かずに実行するとエラーが発生し、アプリがクラッシュします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.tmorimatsu.newsapp"> // ここから <uses-permission android:name="android.permission.INTERNET"/> // ここまでを追加 <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest> |
この状態で実行すると以下のように画面いっぱいに文字が表示されます。
これでAPIが正常に実行されアプリ側でデータを利用できることが確認できました。
リスト表示
次にリストの表示部分を作成していきます。
構成としては以下の図のようになります。
リストで表示するには RecyclerView に対して Adapter をセットする必要があります。この NewsAdapter.kt の役割はリストのそれぞれの行のレイアウトとデータのセットです。Adapter に関する解説は後ほど行います。データは MainActivity.kt から渡され、1行のビューは NewsViewHolder.kt と news_row.xml によって保持・表示されます。
まずはレイアウトファイルから作成します。
レイアウトファイルは res/layout を右クリック -> New -> Layout resource file をクリックします。
news_row.xmlの作成
以下の画像のようにFile nameを変更しOKをクリックします。
作成したらファイルを開き、下の Text タブをクリックします。ちなみに Design タブではドラッグ&ドロップなどの GUI 操作によってレイアウトを作成でき、Text タブでは xml を記述してレイアウトを作成できます。今回は Text タブで以下のようにレイアウトを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/row" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ImageView android:id="@+id/image_row" android:layout_width="100dp" android:layout_height="100dp" android:layout_gravity="center_vertical" android:layout_margin="8dp" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/content_row" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="10dp" android:maxLines="3" android:ellipsize="end"/> <TextView android:id="@+id/publish_row" android:layout_margin="10dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> </LinearLayout> |
LinearLayout は縦・横のどちらかの方向に要素を1列に並べる事ができます。
ここでは LinearLayout を入れ子にすることによって以下のようなレイアウトを作成しています。
具体的にはImageViewと内側のLinearLayoutを横に並べ、内側のLinearLayout内で2つのTextViewを縦に並べています。
NewsViewHolder.ktの作成
こちらのレイアウトファイルと対応するのが NewsViewHolder.kt です。
Name: NewsViewHolder
Kind: Class
にてNewsViewHolder.ktを作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package com.example.tmorimatsu.newsapp import android.support.v7.widget.RecyclerView import android.view.View import android.widget.ImageView import android.widget.TextView class NewsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val imageView = itemView.findViewById<ImageView>(R.id.image_row) val descriptionText = itemView.findViewById<TextView>(R.id.content_row) val publishText = itemView.findViewById<TextView>(R.id.publish_row) val row = itemView.findViewById<View>(R.id.row) } |
NewsAdapter.ktの作成
Name: NewsAdapter
Kind: Class
として新しい Kotlin ファイルを作成します。
こちらのクラスでは RecyclerView.Adapter<NewsViewHolder> を拡張し、コンストラクタにてコンテキストと記事のデータセットを受け取ります。
おそらくリスト表示を行う上で Adapter の存在意義は初見ではわからないのではないかと思います。
そこで、Adapter を利用せずにリストっぽいレイアウトを作る方法を考えてみましょう。
上で作成した news_row.xml のレイアウトを表示する個数分縦に並べることでほぼリストビューと同じようなレイアウトができます。
しかし、これを実現しようとするとリストとして表示する個数が事前に決まっていない限りレイアウトファイルの中で定義しておくことはできず、必然的にコード側から設定することになりますが、その場合にはまず表示する個数を計算し、その数の分だけレイアウトを作成し、作成したレイアウトそれぞれに異なるデータをセットする必要があります。これらももちろんできなくは無いですが、めんどくさいです。そこでこれらの処理をAdapterが肩代わりしてくれます。Adatper は
- onCreateViewHolder : 1行あたりのレイアウトを View として return
- getItemCount : リストに表示する個数をreturn
- onBindViewHolder : それぞれの行のデータをセット
の3つの abstract メソッドで先程の処理を解決しています。今回は以下のように記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
package com.example.tmorimatsu.newsapp import android.content.Context import android.net.Uri import android.support.customtabs.CustomTabsIntent import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.ViewGroup import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions class NewsAdapter(private val context: Context, private val dataset: List<Article>): RecyclerView.Adapter<NewsViewHolder>() { // 1行のレイアウトをセットします override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder { return NewsViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.news_row, parent, false)) } // リストに表示する行数を返します override fun getItemCount(): Int { return dataset.size } // 1行のレイアウトの内容をセットします override fun onBindViewHolder(holder: NewsViewHolder, position: Int) { // この行の記事を取得 val article = dataset[position] // GlideによりimageToUrlの画像を holder.imageView にセット val options = RequestOptions().centerCrop() Glide.with(context).load(article.urlToImage).apply(options).into(holder.imageView!!) // サマリ・公開日をそれぞれセット // (公開日は時間まで含まれているのではじめの10文字を切り抜き年月日のみが表示される用に加工しています) holder.descriptionText!!.text = article.content holder.publishText?.text = article.publishedAt?.substring(0, 10) // 行全体に対してクリック時の処理をセット holder.row?.setOnClickListener { // urlのページを表示 CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(article.url)) } } } |
行のビューに対して取得した画像・テキストを、行がクリックされたときの処理としてCustomChromeTabsでurlのページを開く処理を記述しています。
activity_main.xml, MainActivity.ktの実装
後は MainActivity から RecyclerView にデータをセットすれば完成です。
まずは activity_main を書き換えて RecyclerView を配置します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.mless.newsapp.MainActivity"> <android.support.v7.widget.RecyclerView android:id="@+id/list" android:layout_width="match_parent" android:layout_height="match_parent"/> </android.support.constraint.ConstraintLayout> |
そして、MainActivity の onResponse 内を書き換えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
package com.example.tmorimatsu.newsapp import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.support.v7.widget.DividerItemDecoration import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.RecyclerView import retrofit2.Call import retrofit2.Callback import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // レイアウトファイルのセット setContentView(R.layout.activity_main) // Retrofitクライアントの取得 val retrofit = Retrofit.Builder().baseUrl("https://newsapi.org/").addConverterFactory(GsonConverterFactory.create()).build() // APIエンドポイントの生成 val api = retrofit.create(ApiService::class.java) // 引数によってapiエンドポイントを指定し、リクエスト api.getNews("取得したAPIキー", "jp").enqueue(object: Callback<ResponseData> { // 通信が失敗したときの処理 override fun onFailure(call: Call<ResponseData>?, t: Throwable?) { // 今回は失敗したときは無視しています。 } // 通信が成功したときの処理 override fun onResponse(call: Call<ResponseData>?, response: Response<ResponseData>?) { // レスポンスのnullチェック val res = response?.body() ?: return // NewsAdapterへ渡すデータセットを作成 val dataset = res.articles.filter { !it.content.isNullOrEmpty() } findViewById<RecyclerView>(R.id.list).apply(){ // リストの罫線を設定 addItemDecoration(DividerItemDecoration(this@MainActivity, DividerItemDecoration.VERTICAL)) // 生成したLinearLayoutManagerをセット layoutManager = LinearLayoutManager(this@MainActivity) // RecyclerViewの生成したNewsAdapter をセット adapter = NewsAdapter(this@MainActivity, dataset) } } }) } } |
ここでは記事の content が null もしくはからの場合にはリストに表示するデータとして追加しないようにしています。
また、RecyclerViewの特徴として機能の自由度が高いことがありますが、その反面、リスト項目間の罫線もデフォルトでは存在しなかったり、リストの表示形式やをスクロールさせる方向(縦・横)を決めるLayoutManagerが必要となります。
ここでは apply の内部で罫線の設定・LinearLayoutManager のセット・NewsAdapter のセットを行っています。
お疲れ様でした。以上で実装は完了です。
それではアプリを実行してみましょう!
What’s next?
この章では最後にもっと本格的なアプリを作成するためこの後どのようなことを勉強する必要があるのか、また実際に実務で開発をしてみて感じていることを少しだけお話したいと思います。
データの永続化
ほとんどのアプリで端末でのデータの永続化の処理は必ず必要になってきます。Android にはデフォルトで SQLite が搭載されているためそちらを利用することもできますし、realm などのデータベースを利用することもできます。realm の利用方法については以下が参考になります。
https://realm.io/docs/java/latest/
ライフサイクル
Android の Activity やその他のビューなどにはライフサイクルが存在し、それらの理解なしでは実務で開発を行うことは難しいです。そのため基本的な Activity のライフサイクルなどを早い段階で勉強しておくことは今後開発を行っていく上で必ず役に立つと思います。Activity のライフサイクルはAndroidDeveloper に詳細な説明が記載されていますのでそちらで学習するのが良いかと思います。
https://developer.android.com/guide/components/activities?hl=ja#Lifecycle
アーキテクチャ
実務ではおそらく大半がチーム開発を行うことになると思います。その時、他の人が書いたコードが自分のと異なった書き方であると、コードを理解し、追加で記述するときなどに相当な時間が必要になってしまいます。
実際、Android に限らない話ですが、他の事業会社においても新規事業などの開発では、事業をスケールさせるタイミングでコードの状況が複雑になりすぎてエンハンスに耐えきれなくなり、0から作り直すといった事例もいくつかあります。
そのようにならないためにも、自分の作りたいアプリの特性にあったアーキテクチャを理解し、適用することで効率的な開発を行うことができるようになります。Android のアーキテクチャの理解については以下の本が大変参考になりました。
https://peaks.cc/books/architecture_patterns
デザイン
今回作成したアプリはあまりに簡素で少しがっかりされた方もいるかも知れません。しかし、デザインのトレンドや反映の方法を知ることで、同じようなアプリでも実際に配信されているようなきれいな見た目にすることは可能です。
Android Developer の公式サイトでもデザインのガイドラインが公開されており、その実装方法についても紹介されています。今回扱ったリストであれば以下のページが参考になります。
https://material.io/design/components/lists.html
終わりに
アプリ開発のキャッチアップに関しては通信して画面に表示させることさえできればいろいろなアプリを作成することができるようになります。今回は表示の部分に関してははじめのうちは理解が難しいリストに関してのみを取り扱いました。さらに、取得したデータを永続化する、アプリの状態を考慮する(ライフサイクル)、修正しやすい形でコードを書く、表示の形式をきれいにするといったことができればストアで配布されているようなアプリを作成することもできるようになります。(流石にちょっと足りないかも知れませんが) 実際には理論的なところが膨大な量ありますが、モチベーションや理解のしやすさの観点から、まずはある程度動くものを作り、その後に理論的なことを勉強するのが良いのではないかと考えています。
また、個人の意見ですが、個人で開発を行っている方は開発に関する知識をつける目的であれば、今回作成したアプリや上述した今後学ぶべきことなどを一通り抑えることができた段階でインターンやバイトなどを通して実務経験を積むべきだと考えています。私自身がそうだったように、「まだまだ知らないことが多すぎてエンジニアとして実務を行うのは早すぎる」といったような感情を持っている方は結構いらっしゃると思いますが、個人でアプリを開発していた頃と比べると、配属されて実際に実務でアプリ開発を行うようになりチームメンバーから学んだりや規模の大きいコードでの工夫点などを知ることにより成長できていると感じています。配属後に知っていて当然のようなことを知らないために、プロダクトコードの実装時にバグを埋め込んでしまうといったような経験もありましたが、その時でも、チームでのレビューやバグを発見したときの対応などにより個人のミスはカバーしてもらえ、技術的なサポートもしてもらえるような環境で仕事をできているため、安心感を持ってコーディングを行うことができています。
ここまで読んでいただいた方ありがとうございました。この記事が一人でも多くの方の参考になれば幸いです。