Androidアプリ内でPythonを実行する
あいさつ
初めまして、お馬さんです。大学生やっています。大学で初めてプログラミングを触りました。
初めの記事なので誤字やもっとこうしたほうがいいなどがあるかもしれませんがどうぞお手柔らかにお願いします。
環境
- Android Studio Electric Eel | 2022.1.1
- kotlin: 1.7.0
- minSdk: 24
- targetSdk: 33
最終目標
AndroidでPythonのライブラリを使用できるようにする。
Androidの単一データをPythonで処理し、Androidで取得できるようにする。
Androidの配列データをPythonで処理できるようにする。
Pythonの配列データをAndroidで取得できるようにする。
完成アプリ概要
PythonのライブラリのNumPyを用いて処理を行う。
- 入力した最後のデータの範囲内の乱数を表示
- 入力した複数のデータの標準偏差を表示
- 入力した複数のデータの最大値、最小値を表示
完成画面
ソースコード
言語はKotlin、JetpackComposeを使用してアプリを作成しています。
完成コードの全体は最後に載せます。
データ入力画面の作成
「EmptyComposeActivity」を選択し、Nameは「AndroidAppWithPython」で作成。
「MainContent.kt」ファイルを作成し、Composable関数を記載。
import androidx.compose.runtime.Composable
@Composable
fun MainContent(){
}
MainActivityでMainContentを呼び出す
setContent {
AndroidAppWithPythonTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
// MainContent呼び出し
MainContent()
}
}
}
「MainViewModel.kt」クラスファイルを作成し、ViewModelを継承。
import androidx.lifecycle.ViewModel
class MainViewModel: ViewModel(){
}
MainViewModelに、それぞれ変数を記載。
import androidx.lifecycle.ViewModel
// mutableStateOfを利用するのに必要
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class MainViewModel: ViewModel(){
// 入力中のデータ
var input: String by mutableStateOf("")
// Pythonで処理するデータ(リスト)
var argList: MutableList<Double> = mutableListOf()
// Pythonで処理した結果
var randVal: Int by mutableStateOf(0)
var sd: Double by mutableStateOf(0.0)
var maxVal: Double by mutableStateOf(0.0)
var minVal: Double by mutableStateOf(0.0)
}
MainViewModelをMainAcvitityで取得し、MainContentに引数として渡す。
class MainActivity : ComponentActivity() {
// MainViewModelの取得
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AndroidAppWithPythonTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
// MainContentを呼び出し、viewModelを引数として渡す
MainContent(viewModel)
}
}
}
}
}
MainViewModelを引数として受け取り、データを入力する画面を作成。
@Composable
fun MainContent(viewModel: MainViewModel){
var resultFontSize = 28.sp
var elementFontSize = 20.sp
var fieldSize = 200.dp
var spaceSize = 30.dp
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
// 入力したデータ表示
Text(text = "入力したデータ", fontSize = elementFontSize)
if(viewModel.argList.isNotEmpty()){
var index = viewModel.argList.size-1
var text = "data[" + (index).toString() + "] = " + viewModel.argList[index].round(2).toString()
Text(text = text, fontSize = resultFontSize)
}
Spacer(modifier = Modifier.height(spaceSize))
// データ入力・追加欄
Row(verticalAlignment = Alignment.CenterVertically) {
// 入力
TextField(
modifier = Modifier.width(fieldSize),
value = viewModel.input,
label = { Text("数字を入力", color = Color.Black) },
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Phone),
textStyle = TextStyle(fontSize = elementFontSize),
onValueChange = {viewModel.input = it},
)
Spacer(modifier = Modifier.width(spaceSize))
// 追加
Button(onClick = { /*TODO*/ }) {
Text(text = "追加", fontSize = elementFontSize)
}
}
Spacer(modifier = Modifier.height(spaceSize))
// 処理1
Button(onClick = { /*TODO*/ }) {
Text(text = "乱数取得", fontSize = elementFontSize)
}
// 結果1
Text(text = "乱数: " + viewModel.randVal.toString(), fontSize = resultFontSize)
Spacer(modifier = Modifier.height(spaceSize))
// 処理2
Button(onClick = { /*TODO*/ }) {
Text(text = "標準偏差取得", fontSize = elementFontSize)
}
// 結果2
Text(text = "標準偏差: " + viewModel.sd.round(2).toString(), fontSize = resultFontSize)
Spacer(modifier = Modifier.height(spaceSize))
// 処理3
Button(onClick = { /*TODO*/ }) {
Text(text = "最大・最小値取得", fontSize = elementFontSize)
}
// 結果3
Text(text = "最大値: " + viewModel.maxVal.round(2).toString(), fontSize = resultFontSize)
Text(text = "最小値: " + viewModel.minVal.round(2).toString(), fontSize = resultFontSize)
}
}
// 四捨五入する関数
private fun Double.round(decimals: Int): Double {
val factor = 10.0.pow(decimals)
return (this * factor).roundToInt() / factor
}
追加ボタンを押した時の処理を作成。
class MainViewModel: ViewModel(){
fun addData(){
var num: Double = input.toDouble()
argList.add(num)
input = ""
}
}
追加ボタンを押した時にaddData関数を実行。
// データ入力・追加欄
Row(verticalAlignment = Alignment.CenterVertically) {
// 追加
Button(onClick = {viewModel.addData()}) {
Text(text = "追加", fontSize = elementFontSize)
}
}
現在のアプリ画面
Pythonの導入準備
Pythonを使用できるようにするために、Chaquopyを使用
Chaquopyのドキュメントを参考にbuild.gradleにライブラリを追加
plugins {
// Pythonを使うのに必要
id 'com.chaquo.python' version '14.0.2' apply false
}
plugins {
// Pythonを使うのに必要
id 'com.chaquo.python'
}
android {
defaultConfig {
// Pythonを使うのに必要
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
python {
// デフォルトのバージョンは3.8
version "3.8"
}
}
}
NumPyライブラリを使用するために追記。pip install numpy
と似たような感じ。複数ライブラリを使用したい場合は下に続けてinstall "ライブラリ名"
を記載。
python {
pip {
// install "ライブラリ名"
install "numpy"
}
}
Pythonを使った処理
Pythonを実行するためにMainActivityのcontextが必要なので、MainViewModelで使用できるようにする。
class MainActivity : ComponentActivity() {
//MainActivityのcontextを別のファイルから使用できるように
companion object {
lateinit var context: MainActivity
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// contextを取得
context = this
}
}
受け取った引数に応じて実行するPythonの関数を変更する関数を作成。
package com.example.androidappwithpython
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.chaquo.python.Python
import com.chaquo.python.android.AndroidPlatform
class MainViewModel: ViewModel(){
fun processingByPython(processNum: Int){
// Pythonコードを実行する前にPython.start()の呼び出しが必要
if (!Python.isStarted()) {
Python.start(AndroidPlatform(MainActivity.context))
}
// 実行するスクリプト名
val scriptName = "useNumpy"
// インスタンスを取得
val py = Python.getInstance()
// 指定したスクリプトのモジュールを取得
val module = py.getModule(scriptName)
// 引数に応じて実行する処理を選択
when (processNum) {
// 入力した最後のデータの範囲内の乱数を取得
1 -> {
// 実行する関数名
val funcName = "getRand"
// 指定した関数に引数を渡して実行し、結果をInt型で受け取る
val result = module.callAttr(funcName, argList[argList.size-1].toInt()).toInt()
// 結果を変数に代入
randVal = result
}
// 入力した複数のデータの標準偏差を取得
2 -> {
// 実行する関数名
val funcName = "getSd"
// 指定した関数に引数を渡して実行し、結果をDouble型で受け取る
val result = module.callAttr(funcName, argList).toDouble()
// あとで代入を追記
}
// 入力した複数のデータの最大値、最小値を表示
3 -> {
// 実行する関数名
val funcName = "getMaxAndMin"
// 指定した関数に引数を渡して実行し、結果をString型で受け取る
val result = module.callAttr(funcName, argList).toString()
// あとで代入を追記
}
else -> {
}
}
// 処理が終了したらデータを空にする
argList = emptyList<Double>().toMutableList()
}
}
引数が複数必要な場合はこのように記載。
module.callAttr("関数名", "引数1", "引数2" ・・・)
「AndroidAppWithPython/app/src/main/python」に「useNumpy.py」Pythonファイルを作成し、VisualStudioCodeなどのコードエディターで開く。Pythonにそれぞれの処理関数を作成。
import numpy as np
# 決められた範囲の乱数を取得
def getRand(range):
res = np.random.randint(range)
return res
# 標準偏差を取得
def getSd(dataList):
res = np.std(dataList)
return res
# 最大値、最小値を取得
def getMaxAndMin(dataList):
maxIndex = np.argmax(dataList)
maxVal = dataList[maxIndex]
minIndex = np.argmin(dataList)
minVal = dataList[minIndex]
res = [maxVal, minVal]
return res
取得ボタンを押した時に処理ごとの引数を渡してprocessingByPython関数を実行。
// 処理1
Button(onClick = { viewModel.processingByPython(1) }) {
Text(text = "乱数取得", fontSize = elementFontSize)
}
// 処理2
Button(onClick = { viewModel.processingByPython(2) }) {
Text(text = "標準偏差取得", fontSize = elementFontSize)
}
// 処理3
Button(onClick = { viewModel.processingByPython(3) }) {
Text(text = "最大・最小値取得", fontSize = elementFontSize)
}
実行すると、処理1はできるが2と3はエラーが発生する。
エラー文
com.chaquo.python.PyException: AttributeError: 'ArrayList' object has no attribute 'dtype
ArrayListの属性がないと言われる。Pythonで取得したデータの属性を調べてみる。
# dataListの属性を出力
print(type(dataList))
I/python.stdout: <class 'java.util.ArrayList'>
この属性をPythonで扱うのは困難なので、Pythonで扱いやすいListにPython側で変換。
# java.util.ArrayList → Listに変換
def changeArrayList(arrayList):
pyList = [arrayList.get(i) for i in range(arrayList.size())]
return pyList
changeArrayListにAndroidから渡された引数を渡してPythonのListの属性に変換する。
# 標準偏差を取得
def getSd(dataArrayList):
#Listに変換
dataList = changeArrayList(dataArrayList)
res = np.std(dataList)
return res
# 最大値、最小値を取得
def getMaxAndMin(dataArrayList):
#Listに変換
dataList = changeArrayList(dataArrayList)
maxIndex = np.argmax(dataList)
maxVal = dataList[maxIndex]
minIndex = np.argmin(dataList)
minVal = dataList[minIndex]
res = [maxVal, minVal]
return res
結果を変数に代入。複数の結果を渡したい場合、Stringで取得し、分割する。
// 入力した複数のデータの標準偏差を取得
2 -> {
// 指定した関数に引数を渡して実行し、結果をDouble型で受け取る
val result = module.callAttr(funcName, argList).toDouble()
// 結果を変数に代入
sd = result
}
// 入力した複数のデータの最大値、最小値を表示
3 -> {
// 指定した関数に引数を渡して実行し、結果をString型で受け取る
val result = module.callAttr(funcName, argList).toString()
// あとで代入を追記
// 最初と最後の[]を取り除き、","で分割
val resultList = result.substring(1, result.length - 1).split(",").map { it.trim().toDouble() }
// 結果を変数に代入
maxVal = resultList[0]
minVal = resultList[1]
}
完成!
完成画面
完成コード
完成コード
package com.example.androidappwithpython
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import com.example.androidappwithpython.ui.theme.AndroidAppWithPythonTheme
class MainActivity : ComponentActivity() {
// MainViewModelの取得
private val viewModel: MainViewModel by viewModels()
//MainActivityのinstanceを別のファイルから使用できるように
companion object {
lateinit var context: MainActivity
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
context = this
setContent {
AndroidAppWithPythonTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
// MainContentを呼び出し、viewModelを引数として渡す
MainContent(viewModel)
}
}
}
}
}
package com.example.androidappwithpython
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlin.math.pow
import kotlin.math.roundToInt
@Composable
fun MainContent(viewModel: MainViewModel){
var resultFontSize = 28.sp
var elementFontSize = 20.sp
var fieldSize = 200.dp
var spaceSize = 30.dp
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
// 入力したデータ表示
Text(text = "入力したデータ", fontSize = elementFontSize)
if(viewModel.argList.isNotEmpty()){
var index = viewModel.argList.size-1
var text = "data[" + (index).toString() + "] = " + viewModel.argList[index].round(2).toString()
Text(text = text, fontSize = resultFontSize)
}
Spacer(modifier = Modifier.height(spaceSize))
// データ入力・追加欄
Row(verticalAlignment = Alignment.CenterVertically) {
// 入力
TextField(
modifier = Modifier.width(fieldSize),
value = viewModel.input,
label = { Text("数字を入力", color = Color.Black) },
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Phone),
textStyle = TextStyle(fontSize = elementFontSize),
onValueChange = {viewModel.input = it},
)
Spacer(modifier = Modifier.width(spaceSize))
// 追加
Button(onClick = {viewModel.addData()}) {
Text(text = "追加", fontSize = elementFontSize)
}
}
Spacer(modifier = Modifier.height(spaceSize))
// 処理1
Button(onClick = { viewModel.processingByPython(1) }) {
Text(text = "乱数取得", fontSize = elementFontSize)
}
// 結果1
Text(text = "乱数: " + viewModel.randVal.toString(), fontSize = resultFontSize)
Spacer(modifier = Modifier.height(spaceSize))
// 処理2
Button(onClick = { viewModel.processingByPython(2) }) {
Text(text = "標準偏差取得", fontSize = elementFontSize)
}
// 結果2
Text(text = "標準偏差: " + viewModel.sd.round(2).toString(), fontSize = resultFontSize)
Spacer(modifier = Modifier.height(spaceSize))
// 処理3
Button(onClick = { viewModel.processingByPython(3) }) {
Text(text = "最大・最小値取得", fontSize = elementFontSize)
}
// 結果3
Text(text = "最大値: " + viewModel.maxVal.round(2).toString(), fontSize = resultFontSize)
Text(text = "最小値: " + viewModel.minVal.round(2).toString(), fontSize = resultFontSize)
}
}
// 四捨五入する関数
private fun Double.round(decimals: Int): Double {
val factor = 10.0.pow(decimals)
return (this * factor).roundToInt() / factor
}
package com.example.androidappwithpython
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.chaquo.python.Python
import com.chaquo.python.android.AndroidPlatform
class MainViewModel: ViewModel(){
// 入力中のデータ
var input: String by mutableStateOf("")
// pythonで処理するデータ(リスト)
var argList: MutableList<Double> = mutableListOf()
// pythonで処理した結果
var randVal: Int by mutableStateOf(0)
var sd: Double by mutableStateOf(0.0)
var maxVal: Double by mutableStateOf(0.0)
var minVal: Double by mutableStateOf(0.0)
// データを追加
fun addData(){
var num: Double = input.toDouble()
argList.add(num)
input = ""
}
fun processingByPython(processNum: Int){
// Pythonコードを実行する前にPython.start()の呼び出しが必要
if (!Python.isStarted()) {
Python.start(AndroidPlatform(MainActivity.context))
}
// 実行するスクリプト名
val scriptName = "useNumpy"
// インスタンスを取得
val py = Python.getInstance()
// 指定したスクリプトのモジュールを取得
val module = py.getModule(scriptName)
// 引数に応じて実行する処理を選択
when (processNum) {
// 入力した最後のデータの範囲内の乱数を取得
1 -> {
// 実行する関数名
val funcName = "getRand"
// 指定した関数に引数を渡して実行し、結果をInt型で受け取る
val result = module.callAttr(funcName, argList[argList.size-1].toInt()).toInt()
// 結果を変数に代入
randVal = result
}
// 入力した複数のデータの標準偏差を取得
2 -> {
// 実行する関数名
val funcName = "getSd"
// 指定した関数に引数を渡して実行し、結果をDouble型で受け取る
val result = module.callAttr(funcName, argList).toDouble()
// 結果を変数に代入
sd = result
}
// 入力した複数のデータの最大値、最小値を表示
3 -> {
// 実行する関数名
val funcName = "getMaxAndMin"
// 指定した関数に引数を渡して実行し、結果をString型で受け取る
val result = module.callAttr(funcName, argList).toString()
// あとで代入を追記
// 最初と最後の[]を取り除き、","で分割
Log.d("result", result)
val resultList = result.substring(1, result.length - 1).split(",").map { it.trim().toDouble() }
maxVal = resultList[0]
minVal = resultList[1]
}
else -> {
}
}
// 処理が終了したらデータを空にする
argList = emptyList<Double>().toMutableList()
}
}
buildscript {
ext {
compose_ui_version = '1.2.0'
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.4.0' apply false
id 'com.android.library' version '7.4.0' apply false
id 'org.jetbrains.kotlin.android' version '1.7.0' apply false
// pythonを使うのに必要
id 'com.chaquo.python' version '14.0.2' apply false
}
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
// pythonを使うのに必要
id 'com.chaquo.python'
}
android {
namespace 'com.example.androidappwithpython'
compileSdk 33
defaultConfig {
applicationId "com.example.androidappwithpython"
minSdk 24
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
// pythonを使うのに必要
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
python {
version "3.8"
pip {
install "numpy"
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.2.0'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.1'
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
implementation 'androidx.compose.material:material:1.2.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
}
import numpy as np
# java.util.ArrayList → Listに変換
def changeArrayList(arrayList):
pyList = [arrayList.get(i) for i in range(arrayList.size())]
return pyList
# 決められた範囲の乱数を取得
def getRand(range):
res = np.random.randint(range)
return res
# 標準偏差を取得
def getSd(dataArrayList):
#Listに変換
dataList = changeArrayList(dataArrayList)
res = np.std(dataList)
return res
# 最大値、最小値を取得
def getMaxAndMin(dataArrayList):
#Listに変換
dataList = changeArrayList(dataArrayList)
maxIndex = np.argmax(dataList)
maxVal = dataList[maxIndex]
minIndex = np.argmin(dataList)
minVal = dataList[minIndex]
res = [maxVal, minVal]
return res
おわりに
Androidアプリ内でPythonを実行できるようになりました。これでAndroidでもPythonのライブラリが使用できますね。今回は簡単なNumPyを使いましたが、機械学習が行えるscikit-learnとかも使えたので色々活用してください。
参考サイト
Discussion
素晴らしい記事をありがとうございます。
質問させてください。
アプリサイズ、起動時間等はどのように変化しますか?😲
質問ありがとうございます。
アプリサイズについては、今回のアプリでは75.5MBあり、少々大きくなってしまうかもしれません。
起動時間については、今回のアプリでは体感ですが、あまり変化はありませんでした。ただ、Chaquopy実装後の初回ビルド時は少々時間がかかりました。
ご返信ありがとうございます。
なるほど。
ありがとうございました!