- 投稿日:2020-03-31T20:22:48+09:00
Google Play ConsoleでのANRの調査
Google Play Console
では、アプリのクラッシュやANRの発生ログを確認することができます。
ANRは、クラッシュに比べると発生原因や再現手順を比較的追いにくい場合が多いです。私も、最初はANRのログをどう追えばいいかわからなくて苦労したので、
Google Play Console
でのANRの調査についてまとめました。ANRの基本的な知識
念のため、はじめにANRの基本についても書いておきました。
基本的な内容なので、読み飛ばしていただいでも結構です。ANRとは
ANRは、
Application Not Responding
の略です。
その名の通り、アプリが応答していないことをユーザーに伝えるための仕組みです。ANRが発生すると、「アプリケーション応答なし」のダイアログが表示されます。
「ボタンをタップしたらフリーズした…」といったときに、表示されるダイアログがANRのダイアログです。
ANRの発生条件
ANRの発生条件は以下の2つです。
- 入力イベント(キーの押下や画面タッチなどのイベント)に対する応答が 5 秒以内にない
BroadcastReceiver
の実行が 10 秒以内に終了しないANRの発生要因
ANRは、UIスレッドで実行時間の長い処理を行って処理をブロックしてしまうことが要因で起こります。
UIスレッドは、画面の描画などGUI関連の操作に使用されるスレッドですが、タッチイベントなどユーザーの操作に対してフィードバックを与えるようなイベント処理も行います。
このフィードバックが遅くなると、ユーザーから「なぜか反応しない。固まった。フリーズしたああああ」など異常事態に感じられてしまいます。
なので、素早く処理をしてあげたいところですが、UIスレッドはスレッドなので、同時には1つの処理しかできません。
つまり、このUIスレッドでユーザーのタッチ反応とは別で、長い処理(ネットワーク通信やデータベース関連の処理など)を行ってしまうと、ユーザが画面をタッチしても、タッチのイベント処理がこの長い時間のかかる処理が終わるのを待ってから処理されてしまう...ことになります。
そのために、UIスレッドで実行されるメソッドでは、実行する処理をできる限り少なくする必要があります。
ANRの対策方法
以下の方法が一般的なANRの対策になります。
- 主なライフサイクルメソッド(onCreate() や onResume()など)で設定する内容はできる限り少なくする
- ネットワークやデータベースの処理や、CPUへの負荷が高い計算(ビットマップのサイズ変更など)は、ワーカースレッドで行う
※データベース処理の場合はワーカースレッドもしくは非同期リクエストを使用するでもいいと思います。
UIスレッドで行う処理はなるべく短くして、時間のかかる処理は別スレッドで行う
これがタッチの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)まず、上記のログから、
BaseFragment
のonCreateView()
でANRが発生していることがわかります。(onCreateView()
にそんなに時間がかかっていなければログには表示されないので、onCreateView()
の中の処理に絞ることができる)
onCreateView()
が走っているということは、Fragment
が生成されたタイミングになるので、UIスレッドの処理の最中だったとわかります。そして、
ProgressAnimationLayout.<init>
のinit
となっているのがポイントで、これは view の初期化タイミングを意味します。ということは、
Fragment
のonCreateView()
の中でProgressAnimationLayout
の初期化の中で10秒以上たった = 「Fragment
の View の初期化処理が重い」といった感じで仮説を考えることができます。
全てグレー字でANRのログが表示されている例(内部的な処理の中でANRが発生している場合)
ANRを日々調査していると、
以下のように黒字が全くなく、内部的な処理の最中に発生したANRのログもよくあがってきます。こういう場合も下から順に追って、怪しそうなやつを探します。
今回の場合だと、
at androidx.work.WorkManager.initialize<init>この辺りが怪しそうだなと思います。
WorkManeger
の初期化に時間がかかってANRが発生してしまった可能性が考えられます。例えば、
- アプリが死んでいる状態で、
PlayService
に登録していたWorkManager
が立ち上がるApplication
関係の初期化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 アクティビティを検査するもし間違いなどあればご指摘いただけると嬉しいです。
最後まで読んでいいただき、ありがとうございました。参考文献
- 投稿日:2020-03-31T18:42:59+09:00
アンドロイドで線形回帰モデルを使って推論してみる[PyTorch Mobile]
今回やること
python で線形回帰モデルを作ってそのモデルを使ってアンドロイド上で推論する。(アンドロイド上で学習させるわけではありません。)
今回のコードはgithubに載せているので適宜参照してください。(最下部にURL掲載)
・PyTorch Mobileを使う
モデルの作成
まずはアンドロイドで動かすための線形モデルを作っていく。
python環境がなくアンドロイドの方だけ読みたい方はアンドロイドで推論という見出しまで読み飛ばして、完成したモデルをダウンロードしてください。なお今回掲載するコードはjupyter notebook上で動かしたものです。
データセット
今回使うデータセットはkaggleに載ってた Red Wine Qualityを使ってみる。
酸味、ph、度数などのワインの成分データからワインの10段階の品質を予測する感じ。
今回は単に線形モデルをアンドロイドで動かしてみたいだけなのでシンプルな線形重回帰で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()特に欠損値とかもないので次にデータローダーを作っていく
データローダーの作成
#入力と正解ラベル 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)ちょっとできたモデルを試してみる
for i in range(len(X_test)): print("推論結果:",model(X_test[i]).data, "正解ラベル:", y_test[i].data)んん?全然合ってないな。確かにただの線形重回帰だけどこんなに合わないのかなー。
データをもう一回見てみるとクオリティの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.gradledependencies { implementation 'org.pytorch:pytorch_android:1.4.0' implementation 'org.pytorch:pytorch_android_torchvision:1.4.0' }モデルを入れる
アンドロイドスタジオに先ほどダウンロードまたは作成した学習済みモデル(wineModel.pt)を入れる。
まずはassetフォルダを作る(「resフォルダとか適当な場所を右クリック->新規->フォルダ->assetフォルダ」で作れる)
そこに学習済みモデルをコピペする。
レイアウト
推論結果を表示するレイアウトをつくる。といっても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.ktclass 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 コード
- 投稿日:2020-03-31T16:28:31+09:00
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 です。
- 投稿日:2020-03-31T12:53:11+09:00
初心者の初心者による初心者のための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対応なんちゃって開発者にならないための実践ハンズオン』を参照にしております。