20200331のAndroidに関する記事は4件です。

Google Play ConsoleでのANRの調査

Google Play Consoleでは、アプリのクラッシュやANRの発生ログを確認することができます。
ANRは、クラッシュに比べると発生原因や再現手順を比較的追いにくい場合が多いです。

私も、最初はANRのログをどう追えばいいかわからなくて苦労したので、Google Play ConsoleでのANRの調査についてまとめました。

ANRの基本的な知識

念のため、はじめにANRの基本についても書いておきました。
基本的な内容なので、読み飛ばしていただいでも結構です。

ANRとは

ANRは、Application Not Respondingの略です。
その名の通り、アプリが応答していないことをユーザーに伝えるための仕組みです。

ANRが発生すると、「アプリケーション応答なし」のダイアログが表示されます。
「ボタンをタップしたらフリーズした…:fearful:」といったときに、表示されるダイアログがANRのダイアログです。
image.png

ANRの発生条件

ANRの発生条件は以下の2つです。

  • 入力イベント(キーの押下や画面タッチなどのイベント)に対する応答が 5 秒以内にない
  • BroadcastReceiverの実行が 10 秒以内に終了しない

ANRの発生要因

ANRは、UIスレッドで実行時間の長い処理を行って処理をブロックしてしまうことが要因で起こります。

UIスレッドは、画面の描画などGUI関連の操作に使用されるスレッドですが、タッチイベントなどユーザーの操作に対してフィードバックを与えるようなイベント処理も行います。

このフィードバックが遅くなると、ユーザーから「なぜか反応しない。固まった。フリーズしたああああ:rage::rage:」など異常事態に感じられてしまいます。

なので、素早く処理をしてあげたいところですが、UIスレッドはスレッドなので、同時には1つの処理しかできません。

つまり、このUIスレッドでユーザーのタッチ反応とは別で、長い処理(ネットワーク通信やデータベース関連の処理など)を行ってしまうと、ユーザが画面をタッチしても、タッチのイベント処理がこの長い時間のかかる処理が終わるのを待ってから処理されてしまう...ことになります。

そのために、UIスレッドで実行されるメソッドでは、実行する処理をできる限り少なくする必要があります。

ANRの対策方法

以下の方法が一般的なANRの対策になります。

  • 主なライフサイクルメソッド(onCreate() や onResume()など)で設定する内容はできる限り少なくする
  • ネットワークやデータベースの処理や、CPUへの負荷が高い計算(ビットマップのサイズ変更など)は、ワーカースレッドで行う

※データベース処理の場合はワーカースレッドもしくは非同期リクエストを使用するでもいいと思います。

:point_up:UIスレッドで行う処理はなるべく短くして、時間のかかる処理は別スレッドで行う:point_up:
これがタッチのUXを向上させ。ANRを防ぐ鉄則です。

Google Play consoleでのANRの調査方法

基本的な見方

Google Play console上に表示される、ANRのログの基本的な見方は以下の通りです。

  • 下から順番(降順)に追って見る
  • 黒字で書いてあるものは自分達が書いたコードで、グレーの文字は内部的な処理
  • 最新のエラー発生versionを見て、最初に該当のANRが発生したversionCodeを確認する(既存なのか新規発生なのかがわかる)

ANRを調査するときに意識すること

問題になっていそうな箇所がわかったら、次にその該当箇所のコードを見て、以下のような観点で調査をしていきます。

  • 「黒字」で書かれた自分たちのコードがどういうタイミングで走るのか?
  • UIスレッドで行われているか?
  • 通信周りやDB操作は「非同期」で処理していない所があるか?
  • ログにたくさん出ていないか?(ログにたくさん出ているところは、処理がネストしていることが原因になっていることが多い)

黒字でANRのログが表示されている例(内部的な処理じゃない場合)

ANRのログに黒字でログが出ている場合は、割と原因が追いやすいです。
例として、以下のログがGoogle Play Console上のANRのログ欄に 黒字で 表示されている場合を考えます。

at jp.co.hoge.view.ProgressAnimationLayout.<init> 
at jp.co.hoge.ui.base.BaseFragment.onCreateView (BaseFragment.java:126) 

まず、上記のログから、BaseFragmentonCreateView()でANRが発生していることがわかります。(onCreateView()にそんなに時間がかかっていなければログには表示されないので、onCreateView()の中の処理に絞ることができる)
onCreateView()が走っているということは、Fragmentが生成されたタイミングになるので、UIスレッドの処理の最中だったとわかります。

そして、ProgressAnimationLayout.<init>initとなっているのがポイントで、これは view の初期化タイミングを意味します。

ということは、

FragmentonCreateView()の中で ProgressAnimationLayoutの初期化の中で10秒以上たった = 「Fragment の View の初期化処理が重い」

といった感じで仮説を考えることができます。

全てグレー字でANRのログが表示されている例(内部的な処理の中でANRが発生している場合)

ANRを日々調査していると、
以下のように黒字が全くなく、内部的な処理の最中に発生したANRのログもよくあがってきます。

image.png

こういう場合も下から順に追って、怪しそうなやつを探します。

今回の場合だと、

at androidx.work.WorkManager.initialize<init> 

この辺りが怪しそうだなと思います。
WorkManegerの初期化に時間がかかってANRが発生してしまった可能性が考えられます。

例えば、

  1. アプリが死んでいる状態で、PlayService に登録していた WorkManager が立ち上がる
  2. Application 関係の初期化
  3. Worker の実行中に時間がかかってしまった

といったパターンが考えられます。

また、他のRunnableになっているスレッドをいくつか開いて調査も行います。

  • Runnableになっている Thread(Signal Catcher)をみるとAndroidのBootプロセス系が走っているところだった
  • Runnable になっている Thread(Queue)を見ると RemoteConfigManager.<init>が走っていて、held mutexes = mutator lock(shared held)と書いあった

以上から、

Remote Configの初期化に時間のかかった」かつ、 「mutax がロックされているのでどこかで共有プロセスがロックされて動けない状態が続いた」ことで 10秒経過してANRが発生した

といった感じで、内部的な処理の場合でも仮設を考えられます。

まとめ

以上が、私が日々行なっているGoogle Play ConsoleでのANRの調査でした。
自分なりの見解にはなりますが、ANRの原因調査などに参考になれば嬉しいです。

また、あくまで今回は、Google Play Console上のログから、原因調査をするところまでなので、
この後は、CPU Profilerなどを使用して、自分の仮説が正しいかさらに詳細な調査をしていく流れになっていくと思います。詳細はこちらの公式ページにわかりやすくまとまっています。
CPU Profiler を使用して CPU アクティビティを検査する

もし間違いなどあればご指摘いただけると嬉しいです。
最後まで読んでいいただき、ありがとうございました。:bow:

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アンドロイドで線形回帰モデルを使って推論してみる[PyTorch Mobile]

今回やること

python で線形回帰モデルを作ってそのモデルを使ってアンドロイド上で推論する。(アンドロイド上で学習させるわけではありません。)

今回のコードはgithubに載せているので適宜参照してください。(最下部にURL掲載)

今回作るやつ↓

PyTorch Mobileを使う

pytorch-mobile.png

モデルの作成

まずはアンドロイドで動かすための線形モデルを作っていく。
python環境がなくアンドロイドの方だけ読みたい方はアンドロイドで推論という見出しまで読み飛ばして、完成したモデルをダウンロードしてください。

なお今回掲載するコードはjupyter notebook上で動かしたものです。

データセット

今回使うデータセットはkaggleに載ってた Red Wine Qualityを使ってみる。

酸味、ph、度数などのワインの成分データからワインの10段階の品質を予測する感じ。
キャプチdfasdfadsffdvzscャ.PNG

今回は単に線形モデルをアンドロイドで動かしてみたいだけなのでシンプルな線形重回帰で10段階クオリティを連続値とみて線形モデルでフィッティングしていく。11カラムあるけど特にL1正則化とかは無しで。(うーん、精度悪くなりそう...)

データ整理

データを眺めたり、データの欠損地チェックや、データの整理を行う。

kaggle からダウンロードしたデータのインポート

import torch 
from matplotlib import pyplot as plt
import pandas as pd
import seaborn as sns

wineQualityData = pd.read_csv('datas/winequality-red.csv')

一応相関をプロットしたり、欠損チェックしたり..

sns.pairplot(wineQualityData)

#欠損データのチェック
wineQualityData.isnull().sum()

cvahtr.PNG   yjktkj.PNG

特に欠損値とかもないので次にデータローダーを作っていく

データローダーの作成

#入力と正解ラベル
X = wineQualityData.drop(['quality'], 1)
y = wineQualityData['quality']

#8:2で分ける
X_train = torch.tensor(X.values[0:int(len(X)*0.8)], dtype=torch.float32)
X_test = torch.tensor(X.values[int(len(X)*0.8):len(X)], dtype=torch.float32)

#8:2で分ける
y_train = torch.tensor(y.values[0:int(len(y)*0.8)], dtype=torch.float32)
y_test = torch.tensor(y.values[int(len(y)*0.8):len(y)], dtype=torch.float32)

#データローダー作成
train = torch.utils.data.TensorDataset(X_train, y_train)
train_loader = torch.utils.data.DataLoader(train, batch_size=100, shuffle=True)
test = torch.utils.data.TensorDataset(X_test, y_test)
test_loader = torch.utils.data.DataLoader(test, batch_size=50, shuffle=False)

pytorchにデータローダーを簡単に作れるメソッドが用意されてて楽。
今回一応テスト用データも作ってますが今回は使いません。

モデルの作成

続いて線形モデルを作っていく。

from torch import nn, optim

#モデル
model = nn.Linear(in_features = 11, out_features=1, bias=True)
#学習率
lr = 0.01
#2乗誤差
loss_fn=nn.MSELoss()
#損失関数のログ
losses_train= []
#最適化関数
optimizer = optim.Adam(model.parameters(), lr=lr)

モデルの学習

作成したモデルを学習させる

from tqdm import tqdm
for epoch in tqdm(range(100)):
    print("epoch:", epoch)

    for x,y in train_loader:
        # 前回の勾配をゼロに
        optimizer.zero_grad()
        # 予測
        y_pred = model(x)
        # MSE loss とwによる微分を計算
        loss = loss_fn(y_pred, y)
        if(epoch != 0):  #誤差が小さくなったら終了
            if abs(losses_train[-1] - loss.item()) < 1e-1:
                break
        loss.backward()
        optimizer.step()
        losses_train.append(loss.item())

    print("train_loss", loss.item())

学習結果

損失関数の推移

plt.plot(losses_train)

adsffgda.png
一応収束はしてるっぽい。

ちょっとできたモデルを試してみる

for i in range(len(X_test)):
    print("推論結果:",model(X_test[i]).data, "正解ラベル:", y_test[i].data)

adsfjkasdf.PNG

んん?全然合ってないな。確かにただの線形重回帰だけどこんなに合わないのかなー。
データをもう一回見てみるとクオリティの56%が5だった。つまり、損失を少なくするようにほとんど5の値に収束してしまったのかな。そもそもこういうデータを連続値ラベルとみて線形重回帰するのは厳しかったのか。分類でやった方がよかったかも。

ただ、今回はモデルの精度を求めるのがメインではないので、とりあえずはこれでモデル完成ということにしておく。

もし、今回の精度が悪かった原因がコードのここが悪いよとかわかる方いましたら、コメントで教えてください。

モデルの保存

アンドロイドにモデルを入れるためにモデルを保存する

import torchvision

model.eval()
#入力テンソルのサイズ
example = torch.rand(1,11) 
traced_script_module = torch.jit.trace(model, example)
traced_script_module.save("wineModel.pt")

うまく実行できると同じフォルダないにptファイルが生成されるはず。

アンドロイドで推論

読み飛ばした方はgithubから学習済みモデルをダウンロードしてください。

ここから、アンドロイドスタジオを使っていきます。

依存関係

2020年3月時点

build.gradle
dependencies {
    implementation 'org.pytorch:pytorch_android:1.4.0'
    implementation 'org.pytorch:pytorch_android_torchvision:1.4.0'
}

モデルを入れる

アンドロイドスタジオに先ほどダウンロードまたは作成した学習済みモデル(wineModel.pt)を入れる。

まずはassetフォルダを作る(「resフォルダとか適当な場所を右クリック->新規->フォルダ->assetフォルダ」で作れる)
そこに学習済みモデルをコピペする。
jkjjkl.PNG

レイアウト

推論結果を表示するレイアウトをつくる。といってもtextView3個並べただけ。

activity_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">

    <TextView
        android:id="@+id/result"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:textSize="24sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/label" />

    <TextView
        android:id="@+id/label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView"
        android:textSize="30sp"
        app:layout_constraintBottom_toTopOf="@+id/result"
        app:layout_constraintEnd_toEndOf="@+id/result"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="@+id/result"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="赤ワイン品質予測"
        android:textSize="30sp"
        app:layout_constraintBottom_toTopOf="@+id/label"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

推論

モデルをロードして、テンソルを入れて推論

MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //ワインのテスト用データ
        val inputArray = floatArrayOf(7.1f, 0.46f, 0.2f, 1.9f, 0.077f, 28f, 54f, 0.9956f, 3.37f, 0.64f, 10.4f)
        //テンソルの生成: 引数(floatArray, テンソルのサイズ)
        val inputTensor = Tensor.fromBlob(inputArray, longArrayOf(1,11))
        //モデルのロード
        val module = Module.load(assetFilePath(this, "wineModel.pt"))
        //推論
        val outputTensor = module.forward(IValue.from(inputTensor)).toTensor()
        val scores = outputTensor.dataAsFloatArray
        //結果の表示
        result.text ="予測値: ${scores[0]}"
        label.text = "正解ラベル:6"
    }

    //assetフォルダからパスを取得する関数
    fun assetFilePath(context: Context, assetName: String): String {
        val file = File(context.filesDir, assetName)
        if (file.exists() && file.length() > 0) {
            return file.absolutePath
        }
        context.assets.open(assetName).use { inputStream ->
            FileOutputStream(file).use { outputStream ->
                val buffer = ByteArray(4 * 1024)
                var read: Int
                while (inputStream.read(buffer).also { read = it } != -1) {
                    outputStream.write(buffer, 0, read)
                }
                outputStream.flush()
            }
            return file.absolutePath
        }
    }
}

完成!!
ここまで来て実行すると冒頭の画面が出てくるはず。

おわり

画像系はチュートリアルでもあるけど、普通の線形とかはあまり載ってなかったのでこの記事を書いてみた。
モデルの精度がイマイチだったのが引っかかる所だけど、一応線形モデルを動かすことができた。
今度は分類をやってみようかな。

今回のコードはこちら
pythonコード
android studio コード

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android + Bitrise はじめのいっぽ

Bitrise

Bitrise はモバイルアプリ開発に特化した CI/CD ツールです
https://www.bitrise.io/
日本語のドキュメント : https://devcenter.bitrise.io/jp/

特徴

構築が簡単

Jenkinse などのようにサーバーを立てたりする必要がありません。
また、ワークフローの構築がコンソール上の操作のみでできます。

モバイルアプリに特化

モバイルアプリ用のワークフローがすでに組まれています。
Google Play Store、App Store へのアップロード、 Test Flight を使った配信などを組み込むことができます。
Amazon Device Farm を使ったテストにも対応しています。

プラン

  • Hobby : $0/月
1ビルド10分まで, 200 ビルド/月, 1チーム2人まで
  • Developer : $36/月
1ビルド45分まで, 回数制限なし, チーム人数制限なし
  • Org Standard : $90/月
1ビルド90分まで。ビルド用のサーバーが強くなります。
  • Org Elite : $270/月 制限は Standard と同じ。ビルド用のサーバーがさらに強くなります。

Android の CI 環境として Bitrise を使う

すでに述べたように Android の基本的なワークフローは Bitrise 側ですでに組んでくれているので、 Bitrise にアクセスし、サインアップ・リポジトリの選択・ビルド設定の入力を行うだけでビルドを走らせることができます。

ステップの追加

Bitrise が組んでくれたワークフローはユニットテストを実行しログを出してくれるだけの単純なものです。
それ以外の作業を行いたい場合は自分で追加する必要があります。
ダッシュボードからアプリを選択し Workflows タブを選択すると既存のワークフローが表示されます。
ステップを追加したい場所を選び表示される項目の中からそこで行いたいものを選ぶことでステップを追加することができます。

リポジトリのイベント連携

リポジトリへのプッシュ、プルリクエストなどのイベントとビルドを連携させたい場合は Trigger タブで設定できます。
Trigger タブでイベントとワークフローを紐づければ OK です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者の初心者による初心者のためのAndroidアプリ開発

はじめに

最近はコロナウイルスのおかげで勉強がはかどっています笑 2回の投稿を終えてAndroidアプリ開発もそれなりに進んだ内容となってきました。そして、今回は既存のウェブサイトと連携したアプリを作成していきたいと思います。Androidアプリの方で都市を選択すると対応した現在の天気情報を取得して表示するという形式のものです。実用性のあるものなので頑張っていきましょう。

AsyncTaskの作成

今回のアプリの初期状態では、次の画像のように都市のリストが上半分に表示され、下半分にウェブサイトから天気情報を取得して表示するようになっています。

文字列情報の追加とレイアウトファイルの編集は前回までの学習内容でできると思いますので頑張ってみてください。

res/values/strings.xml

<resources>
    <string name="app_name">
        全国の天気
    </string>
    <string name="tv_winfo_title">
        お天気情報
    </string>
</resources>

res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/lvCityList"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="0.5"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginTop="10dp"
        android:layout_weight="0.5"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:gravity="center"
            android:text="@string/tv_winfo_title"
            android:textSize="25sp"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tvCityName"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="20sp"/>

            <TextView
                android:id="@+id/tvWeatherTelop"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:textSize="20sp"/>
        </LinearLayout>

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:id="@+id/tvWeatherDesc"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:textSize="15sp"/>
        </ScrollView>
    </LinearLayout>
</LinearLayout>

次にアクティビティ処理を記述していきます。

java/com.websarva.wings.android.a03_16_Asyncsample/MainActivity

package com.websarva.wings.android.a03_16_asyncsample

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ListView
import android.widget.SimpleAdapter
import android.widget.TextView

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val lvCityList = findViewById<ListView>(R.id.lvCityList)
        val cityList: MutableList<MutableMap<String, String>> = mutableListOf()

        var city = mutableMapOf("name" to  "東京", "id" to "2")
        cityList.add(city)

        city = mutableMapOf("name" to "大阪", "id" to "2")
        cityList.add(city)

        city = mutableMapOf("name" to "名古屋", "id" to "2")
        cityList.add(city)

        val from = arrayOf("name")
        val to = intArrayOf(android.R.id.text1)

        val adapter = SimpleAdapter(applicationContext, cityList, android.R.layout.simple_expandable_list_item_1, from, to)
        lvCityList.adapter = adapter
        lvCityList.onItemClickListener = ListItemClickListener()
    }

    private inner class ListItemClickListener : AdapterView.OnItemClickListener
    {
        override fun onItemClick(parent: AdapterView<*>, view: View?, position: Int, id: Long)
        {
            val item = parent.getItemAtPosition(position) as Map<String, String>
            val cityName = item["name"]
            val cityId = item["id"]

            val tvCityName = findViewById<TextView>(R.id.tvCityName)
            tvCityName.setText(cityName + "の天気: ")
        }
    }
}

今回使用しているウェブサイトのライブドアの天気情報の仕様は、http://weather.livedoor.com/forecast/webservice/json/v1というURLの後ろにパラメータとしてcity=都市のIDを付与してGETリクエストを送信すると天気情報がJSONデータとして返ってくるものです。

サンプルで載せているコードは東京、大阪、名古屋の三都市のみですが、他の都市の情報を表示したい場合は次のサイトを参考にしてみてください。
https://www.kaden1000.com/2013/04/livedoor-weather-hacks1%E6%AC%A1%E7%B4%B0%E5%88%86%E5%8C%BA%EF%BC%88city%E3%82%BF%E3%82%B0%EF%BC%89%E3%81%AE%E5%9C%B0%E5%9F%9Fid%E4%B8%80%E8%A6%A7%E3%81%BE%E3%81%A8%E3%82%81/

非同期処理コードの記述

はじめに、AsyncTaskを使うためのAsyncTaskを継承したメンバクラスを作成していきます。まずはメンバクラスの作成のみで、実際にインターネットに接続して天気情報サービスからデータを取得するコードと取得したJSON文字列を解析するコードは後半で追記していきます。

java.com.websarva.wings.android.a03_16_Asyncsample/MainActivity.kt

            val tvCityName = findViewById<TextView>(R.id.tvCityName)
            tvCityName.setText(cityName + "の天気: ")
        }
    }

        〜以降追記〜
    private inner class WeatherInfoReceiver(): AsyncTask<String, String, String>()
    {
        override fun doInBackground(vararg params: String?): String
        {
            val id = params[0]
            val urlStr = "http://weather.livedoor.com/forecast/webservice/json/v1?city=${id}"
            return result
        }

        override fun onPostExecute(result: String?)
        {
            val tvWeatherTelop = findViewById<TextView>(R.id.tvWeatherTelop)
            val tvWeatherDesc = findViewById<TextView>(R.id.tvWeatherDesc)
            tvWeatherTelop.text = telop
            tvWeatherDesc.text = desc
        }
    }
}

次に、先ほど作成したWeatherInfoReceiverを実行する処理を記述していきます。リストがタップされたときに実行する処理なのでListItemClickListenerメンバクラスのonItemClick()メソッドの末尾にコードを追記していきます。

java.com.websarva.wings.android.a03_16_Asyncsample/MainActivity.kt

            val tvCityName = findViewById<TextView>(R.id.tvCityName)
            tvCityName.setText(cityName + "の天気: ")

            〜以降追記〜
            val receiver = WeatherInfoReceiver()
            receiver.execute(cityId)
        }
    }

HTTP接続の実装

非同期処理の準備ができたのでインターネットに接続して天気情報を取得していきたいと思います。
インターネットに接続して天気情報を取得する処理はバックグラウンドで行うためdoInBackground()に記述します。

java.com.websarva.wings.android.a03_16_Asyncsample/MainActivity.kt

            val id = params[0]
            val urlStr = "http://weather.livedoor.com/forecast/webservice/json/v1?city=${id}"

            〜以降追記〜
            val url = URL(urlStr)
            val con = url.openConnection() as HttpURLConnection

            con.requestMethod = "GET"
            con.connect()

            val stream = con.inputStream
            val result = is2String(stream)

            con.disconnect()
            stream.close()

次に、InputStreamオブジェクトを文字列に変換するprivateメソッドをWeatherInfoReceiverクラスに追記します。

java.com.websarva.wings.android.a03_16_Asyncsample/MainActivity.kt

            tvWeatherTelop.text = telop
            tvWeatherDesc.text = desc
        }
    }

            〜以降追記〜    
    private fun is2String(stream: InputStream): String
    {
        val sb = StringBuilder()
        val reader = BufferedReader(InputStreamReader(stream, "UTF-8"))
        var line = reader.readLine()

        while (line != null)
        {
            sb.append(line)
            line = reader.readLine()
        }

        reader.close()
        return sb.toString()
    }
}

また、doInBackground()で取得したJSON文字列を解析するために、onPostExcute()に処理を記述します。

java.com.websarva.wings.android.a03_16_Asyncsample/MainActivity.kt

        override fun onPostExecute(result: String?)
        {

            〜以降追記〜
            val rootJSON = JSONObject(result)
            val descriptionJSON = rootJSON.getJSONObject("description")
            val desc = descriptionJSON.getString("text")
            val forecasts = rootJSON.getJSONArray("forecasts")
            val forecastNow = forecasts.getJSONObject(0)
            val telop = forecastNow.getString("telop")

最後に、AndroidManifestにタグと属性を追記しましょう。
Androidではアプリがインターネットと接続するには、ぞの許可をアプリに与える必要があります。そのため、AndroidManifest.xmlに次の2つのタグとapplicationのタグ内に属性を追記します。

manifests/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.websarva.wings.android.a03_16_asyncsample">

            〜以降追記〜
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

    <application
        android:usesCleartextTraffic="true"
        android:allowBackup="true"

では、アプリを起動していきましょう!

完成です!

最後に

今回はコードを書く量が多めだったので諦めそうになった方もいるかもしれませんが、完成品を見ると頑張ったなと思えるかもしれません。実用性のあるアプリなので使ってみてはいかがでしょうか。

本記事は、 『基礎&応用力をしっかり育成!Androidアプリ開発の教科書Kotlin対応なんちゃって開発者にならないための実践ハンズオン』を参照にしております。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む