以下で新規にプロジェクトを作成
プロジェクトの選択: Empty Activity
プロジェクトの名前: reader
ビューバインディングを使えるようにする
build.gradle を変更したら「Sync Now」をクリック
画像表示のために、Picassoを使えるようにする
同ファイルの dependencies 内の implementation の一覧の最後に以下を追加
implementation 'com.squareup.picasso:picasso:2.71828'
build.gradle を変更したら「Sync Now」をクリック
なお、バージョンとして「2.5.2」を指定すると「Sync Now」は表示されなかった
https://github.com/square/picasso を参考に、「2.71828」を指定すると表示された
Webページ表示のために、Chrome Custom Tabsを使えるようにする
同ファイルの dependencies 内の implementation の一覧の最後に以下を追加
implementation 'androidx.browser:browser:1.0.0'
build.gradle を変更したら「Sync Now」をクリック
今も「androidx.browser:browser:1.0.0」の指定で正しいかどうかは確認しておきたい
なお「implementation 'com.android.support:customtabs:27.1.1'」という指定は古い方法だと思われる
APK creation failure due to Duplicate class android.support - Stack Overflow
https://stackoverflow.com/questions/59239626/apk-creation-failure-due-to-duplicate-class-android-sup...
AWS CognitoにAndroidネイティブアプリでサインインする - Qiita
https://qiita.com/poruruba/items/12985ec474364594be55
マニフェストファイルを編集し、インターネットに接続できるようにする
追加場所は、ルートであるmanifestの直下でいい
manifests/AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
リスト用のレイアウトを作成する
res/layout/list_article_cell.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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="wrap_content">
<TextView
android:id="@+id/titleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/imageView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.08"
tools:text="ニュースタイトル" />
<TextView
android:id="@+id/nameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleView"
tools:text="名前" />
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.98"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@android:drawable/ic_popup_sync" />
</androidx.constraintlayout.widget.ConstraintLayout>
メイン画面のレイアウトを調整する
res/layout/lactivity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/articlesView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/list_article_cell" />
</androidx.constraintlayout.widget.ConstraintLayout>
app/java/org.refirio.reader/HttpTask.kt
package org.refirio.reader
import android.os.AsyncTask
import android.util.Log
import java.io.*
import java.net.HttpURLConnection
import java.net.URL
class HttpTask(callback: (String?) -> Unit) : AsyncTask<String, Unit, String>() {
var callback = callback
override fun doInBackground(vararg params: String): String? {
val url = URL(params[1])
val httpClient = url.openConnection() as HttpURLConnection
httpClient.setReadTimeout(10 * 1000)
httpClient.setConnectTimeout(10 * 1000)
httpClient.requestMethod = params[0]
if (params[0] == "POST") {
httpClient.instanceFollowRedirects = false
httpClient.doOutput = true
httpClient.doInput = true
httpClient.useCaches = false
httpClient.setRequestProperty("Content-Type", "application/json; charset=utf-8")
}
try {
if (params[0] == "POST") {
httpClient.connect()
val os = httpClient.getOutputStream()
val writer = BufferedWriter(OutputStreamWriter(os, "UTF-8"))
writer.write(params[2])
writer.flush()
writer.close()
os.close()
}
if (httpClient.responseCode == HttpURLConnection.HTTP_OK) {
val stream = BufferedInputStream(httpClient.inputStream)
val data: String = readStream(inputStream = stream)
return data
} else {
Log.d("HttpTask", "ERROR: ${httpClient.responseCode}")
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
httpClient.disconnect()
}
return null
}
fun readStream(inputStream: BufferedInputStream): String {
val bufferedReader = BufferedReader(InputStreamReader(inputStream))
val stringBuilder = StringBuilder()
bufferedReader.forEachLine { stringBuilder.append(it) }
return stringBuilder.toString()
}
override fun onPostExecute(result: String?) {
super.onPostExecute(result)
callback(result)
}
}
app/java/org.refirio.reader/ArticleAcapter.kt
package org.refirio.reader
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.squareup.picasso.Picasso
import org.json.JSONArray
import org.json.JSONObject
import java.text.ParseException
import java.text.SimpleDateFormat
class ArticleAcapter(
context: Context,
private val articles: JSONArray,
private val onItemClicked: (JSONObject) -> Unit
) : RecyclerView.Adapter<ArticleAcapter.ViewHolder>() {
// レイアウトからViewを生成するInflater
private val inflater = LayoutInflater.from(context)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// Viewを生成する
val view = inflater.inflate(R.layout.list_article_cell, parent, false)
// ViewHolderを作る
val viewHolder = ViewHolder(view)
// Viewをタップしたときの処理
view.setOnClickListener {
// アダプター上の家を得る
val position = viewHolder.adapterPosition
// 位置に応じたデータを得る
val article = articles.getJSONObject(position)
// コールバックを呼び出す
onItemClicked(article)
}
return viewHolder
}
override fun getItemCount() = articles.length()
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// 位置に応じたデータを得る
val article = articles.getJSONObject(position)
// 表示内容を更新する
holder.title.text = article.getString("title")
holder.name.text = "by " + article.getString("name") + " at " + article.getString("datetime").toDateTime("MM/dd HH:mm")
Picasso.get()
.load(article.getString("image")) // 画像のURL
.resize(100, 100) // 表示サイズを指定
.centerCrop() // 中央から切り出し
.into(holder.image) // imageViewに流し込み
}
// Viewへの参照を持っておくViewHolder
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title = view.findViewById<TextView>(R.id.titleView)
val name = view.findViewById<TextView>(R.id.nameView)
val image = view.findViewById<ImageView>(R.id.imageView)
}
}
fun String.toDateTime(pattern: String = "yyyy/MM/dd HH:mm:ss"): String {
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val datetime = try {
dateFormat.parse(this)
} catch (e: ParseException) {
null
}
if (datetime == null) {
return ""
} else {
return SimpleDateFormat(pattern).format(datetime)
}
}
app/java/org.refirio.reader/MainActivity.kt
package org.refirio.reader
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import androidx.recyclerview.widget.LinearLayoutManager
import org.json.JSONObject
import org.refirio.reader.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
// JSONを読み込み
HttpTask({
if (it == null) {
println("Data is empty.")
return@HttpTask
}
// JSONを解析する
val json = JSONObject(it)
val articles = json.getJSONArray("articles")
// RecyclerViewをレイアウトから探す
//val recyclerView = findViewById<RecyclerView>(R.id.articles)
// RecyclerViewにアダプターをセットする
binding.articlesView.adapter = ArticleAcapter(this, articles) { article ->
// 記事をタップしたらChrome Custom Tabsで開く
val intent = CustomTabsIntent.Builder().build()
intent.launchUrl(this, Uri.parse(article.getString("url")))
}
// レイアウトマネージャーをセットする
binding.articlesView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
}).execute("GET", "https://refirio.org/reader/?type=json")
}
}