Flutterでアプリ作るメモ
スクラップ概要
Flutterアプリの制作過程を備忘録的に記録したもの。
簡単なアプリを作成予定
作るもの
概要
英語の発音を練習するためのスマホアプリ
使い方の想定
- ユーザがスマホに英語で話しかける
- アプリはユーザの英語を文字起こし(ここでちゃんと文字起こしできるかで、ユーザは英語の発音が正しいかどうかを確認できる)
というシンプルなアプリ。
追加機能として、英会話モードを考え中。
英会話モードでは、ユーザの音声入力に対してAIが返答を考えて送信。それに対して更にユーザが話して……というものを考えている。
返答にはML Kitのスマートリプライを使おうと考え中。
また、英会話モードを追加することも考えると、UIはチャットアプリのようなものを想定
ここ数日分のやつをまとめて投稿
Flutterで英語音声の文字起こしをする。
pub.devにあるspeech_to_textを使う。
入力音声の言語は本体設定の言語に依存している。
今回は本体設定の言語関係無く英語入力にしたいので、以下のコードをlisten部分辺りに書く
await _speechToText.listen(
onResult: _onSpeechResult,
localeId: "en_US",
);
この使用可能なlocalId
の一覧がどこから見ることができるのか不明。
公式のSwitching Recognition Languageに載っているlocales()
関数はその本体設定の言語を取得するっぽい??
英語にしたかったので、適当にen_US
にする。
speech_to_textがどこまで対応しているかは分からないが、このlocaleIdの表記はGCPのSpeechToTextのBCP-47と同じっぽい。
これからやることメモ
- 最低限表示させるためのUIを作る
- 返信を表示させる
- 本格的なUIの作り込み
一週間で終わるかなぁ…?
Flutterで簡単に使えそうなチャットUIは、flutter_chat_uiとchat_bubblesの二つだと感じた。
今回のアプリではchat_bubblesの方を採用。その理由も含めて、これらを軽く紹介する。
flutter_chat_ui
メリット
- 本格的な見た目のチャットUIが簡単に作れる
- exampleだけで簡単なチャットアプリになる
- デザインが綺麗
デメリット
- 画面構成の自由度が低い(自分が使い方を分かっていなかっただけの可能性もある)
- 今回の場合、入力が音声なので入力用のテキストボックスが不要なのだが、消したり他のボタンに変更する方法が分からなかった
- 入力欄をタップした時に何かしらのアクションをする機能が無かった
chat_bubbles
メリット
- 画面構成の自由度が高い
- チャットのバブル部分(メッセージを表示する所)だけ使うことが可能
デメリット
- exampleがflutter_chat_uiに比べてやや不親切
- 甘えなのは分かっているが、画面にバブルを追加する関数も置いて欲しかった……
- デザイン性が作り手次第
- exampleそのまま使うのは少し微妙なデザインしている
今回はチャットの入力欄が不要で、バブル部分のみ使いたかったので、chat_bubblesの方を採用
前述のchat_bubblesを使って、UI部分のみを作っていく
配色はColor Huntというサイトのこの色を採用。
完成イメージ
コード
事前準備
- 空のFlutterアプリを作成
-
chat_bubblesをインストール
-
flutter pub add chat_bubbles
コマンドを実行
-
コード構成
libの下にmain.dart
とbubbles.dart
を入れる。
コード(長いので折りこみ)
main.dart
import 'package:flutter/material.dart';
import 'bubbles.dart';
import 'package:chat_bubbles/chat_bubbles.dart';
void main() {
// runApp関数でウィジェットツリーのルートとなる。
runApp(const App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
// stateLessWidget または stateFulWidgetを継承した場合は必ずbuildメソッドをオーバーライドしないといけない。
Widget build(BuildContext context) {
return const MaterialApp(
home: HomeScreen(),
);
}
}
// 途中で値が変わったらそれを反映させる必要があるのでStatefulWidgetを記載する
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
// StatefulWidgetはWidgetにstateの概念をいれて拡張したもの
// StatefulWidgetはcreateStateメソッドを持ち、これがStateクラスを返す
////stateを継承している型
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// メッセージを格納するための可変長リストを作成
final _bubbles = List.empty(
growable: true,
);
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color.fromARGB(255, 255, 255, 255), //背景色
//タイトルバー
appBar: AppBar(
title: const Text('English Practice'),
backgroundColor: const Color(0xFF96B6C5),
),
body: ListView.builder(
//生成する個数を指定する
itemCount: _bubbles.length,
// ↓itemBuilderの関数型の定義 Widget型を返す
// ここで再描画用のBubbleSpecialThreeをbubblesの個数だけ作成する
itemBuilder: (BuildContext context, int index) {
// 自分の発言の場合、青背景に白文字
if (_bubbles[index].isSender) {
return BubbleSpecialThree(
text: _bubbles[index].message,
isSender: _bubbles[index].isSender,
color: const Color(0xFF96B6C5),
tail: true,
textStyle:
const TextStyle(color: Color(0xFFF4F2DE), fontSize: 20),
);
}
// 自分以外の発言の場合、白背景に青文字
else {
return BubbleSpecialThree(
text: _bubbles[index].message,
isSender: _bubbles[index].isSender,
color: const Color(0xFFF4F2DE),
tail: true,
textStyle:
const TextStyle(color: Color(0xFF96B6C5), fontSize: 20),
);
}
},
),
floatingActionButton: FloatingActionButton(
backgroundColor: const Color(0xFFE9B384),
onPressed: () {
// setState()を呼び出すことで裏では再度buildメソッドを呼び出して変更された変数で再描画している
setState(() {
// 自分のチャットバブル
_bubbles.add(
Bubbles("me", true),
);
// 自分以外のチャットバブル
_bubbles.add(
Bubbles("you", false),
);
});
},
child: const Icon(Icons.mic),
));
}
}
bubbles.dart
class Bubbles {
String message;
bool isSender;
Bubbles(this.message, this.isSender);
}
次すること
右下のボタンを押せば音声入力がONになり、音声で聞き取った内容を画面上に表示するようにする
Speech to textのlisten関数のパラメータ日本語メモ。
自分用なので、自分がいじった部分しか載せていない。他にも色々あるので、気になる場合は下リンクよりどうぞ。
参考元
-
onResult
:単語が認識されたときに呼び出されるメソッドを書く -
parceFor
:単語が聞こえなくなってから何秒間、音声認識状態であるのかを設定する。 -
localeId
:どこの国の何語を聞き取るか設定する
前述のソースコードを変更し、音声認識の結果をチャットに表示する。
コード
前提
- 前述のbubbles.dartは変更なし。
-
speech_to_textをインストール
flutter pub add speech_to_text
変更部分
- main.dart
- ios\Runner\Info.plist
- android\app\src\main\AndroidManifest.xml
コード
Info.plistとAndroidManifest.xmlはspeech_to_textのPermission部分とやっていることが同じです。
Info.plist
iosで音声認識を使用するための設定。
Info.plist(一部のみ)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!--変更部分ここから-->
<key>NSSpeechRecognitionUsageDescription</key>
<string>This permission is required to use voice analysis.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for voice recognition</string>
<!--変更部分ここまで-->
以下略(デフォルトから変更なし)
AndroidManifest.xml
androidで音声認識を使用するための設定。
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!--変更部分ここから-->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<!--変更部分ここまで-->
<application 以下略(デフォルトから変更なし)
main.dart
Widget内はfloatingActionButton部分のみ変更。
あとはWidgetより上部分に関数、変数をいくつか追加。
main.dart
import 'package:flutter/material.dart';
import 'bubbles.dart';
import 'package:chat_bubbles/chat_bubbles.dart';
import 'package:speech_to_text/speech_recognition_result.dart';
import 'package:speech_to_text/speech_to_text.dart';
void main() {
// runApp関数でウィジェットツリーのルートとなる。
runApp(const App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
// stateLessWidget または stateFulWidgetを継承した場合は必ずbuildメソッドをオーバーライドしないといけない。
Widget build(BuildContext context) {
return const MaterialApp(
home: HomeScreen(),
);
}
}
// 途中で値が変わったらそれを反映させる必要があるのでStatefulWidgetを記載する
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
// StatefulWidgetはWidgetにstateの概念をいれて拡張したもの
// StatefulWidgetはcreateStateメソッドを持ち、これがStateクラスを返す
////stateを継承している型
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// メッセージを格納するための可変長リストを作成
final _bubbles = List.empty(
growable: true,
);
final SpeechToText _speechToText = SpeechToText(); // 音声認識をする
String _lastWords = ''; // 認識結果を格納
//アプリ起動時に実行される
void initState() {
super.initState();
_initSpeech();
}
// speechToTextを初期化する
void _initSpeech() async {
await _speechToText.initialize();
}
/// 音声認識が開始されると呼び出される
void _startListening() async {
//TODO: speechtotextnotinitializedexceptionのキャッチ
await _speechToText.listen(
onResult: _onSpeechResult, // 単語が認識されると呼び出される
pauseFor: const Duration(seconds: 3), // 何秒音声が聞こえなかったら終了するかを設定
localeId: "en_US", // 認識言語を英語(アメリカ)にする
);
}
// 音声認識の終了ボタンが押されると呼び出される
void _stopListening() async {
await _speechToText.stop();
}
// 認識した単語を処理する
void _onSpeechResult(SpeechRecognitionResult result) {
setState(() {
// 最終結果が取得できた場合
if (result.finalResult) {
// 認識した単語を取得
_lastWords = result.recognizedWords;
// 何かしらの単語を取得したら、チャット表示
if (_lastWords != '') {
_bubbles.add(
Bubbles(_lastWords, true),
);
}
}
});
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color.fromARGB(255, 255, 255, 255), //背景色
//タイトルバー
appBar: AppBar(
title: const Text('English Practice'),
backgroundColor: const Color(0xFF96B6C5),
),
body: ListView.builder(
//生成する個数を指定する
itemCount: _bubbles.length,
// ↓itemBuilderの関数型の定義 Widget型を返す
// ここで再描画用のBubbleSpecialThreeをbubblesの個数だけ作成する
itemBuilder: (BuildContext context, int index) {
// 自分の発言の場合、青背景に白文字
if (_bubbles[index].isSender) {
return BubbleSpecialThree(
text: _bubbles[index].message,
isSender: _bubbles[index].isSender,
color: const Color(0xFF96B6C5),
tail: true,
textStyle:
const TextStyle(color: Color(0xFFF4F2DE), fontSize: 20),
);
}
// 自分以外の発言の場合、白背景に青文字
else {
return BubbleSpecialThree(
text: _bubbles[index].message,
isSender: _bubbles[index].isSender,
color: const Color(0xFFF4F2DE),
tail: true,
textStyle:
const TextStyle(color: Color(0xFF96B6C5), fontSize: 20),
);
}
},
),
floatingActionButton: FloatingActionButton(
backgroundColor: const Color(0xFFE9B384),
//マイクボタンが押されたとき、Listen状態でなければstartListeninig、そうでなければstopListeningする
onPressed:
_speechToText.isNotListening ? _startListening : _stopListening,
child: Icon(_speechToText.isNotListening ? Icons.mic_off : Icons.mic),
));
}
}
次すること
自動返信機能を追加する
やったのだいぶ前であんまり覚えていないので、書き漏れあったらすみません。
コード
前提
- 前述のbubbles.dartは変更なし。
- main.dartと同じフォルダにreply.dartを追加
-
google_mlkit_smart_replyをpub add
flutter pub add google_mlkit_smart_reply
変更部分
- main.dart
- reply.dart(新規)
コード
main.dart
恐らく_onSpeechResult()のcreateReplyの部分だけ変更したと思う。
main.dart
import 'package:flutter/material.dart';
import 'bubbles.dart';
import 'reply.dart';
import 'package:chat_bubbles/chat_bubbles.dart';
import 'package:speech_to_text/speech_recognition_result.dart';
import 'package:speech_to_text/speech_to_text.dart';
void main() {
// runApp関数でウィジェットツリーのルートとなる。
runApp(const App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
// stateLessWidget または stateFulWidgetを継承した場合は必ずbuildメソッドをオーバーライドしないといけない。
Widget build(BuildContext context) {
return const MaterialApp(
home: HomeScreen(),
debugShowCheckedModeBanner: false,
);
}
}
// 途中で値が変わったらそれを反映させる必要があるのでStatefulWidgetを記載する
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
// StatefulWidgetはWidgetにstateの概念をいれて拡張したもの
// StatefulWidgetはcreateStateメソッドを持ち、これがStateクラスを返す
////stateを継承している型
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// メッセージを格納するための可変長リストを作成
final _bubbles = List.empty(
growable: true,
);
final SpeechToText _speechToText = SpeechToText(); // 音声認識をする
String _lastWords = ''; // 認識結果を格納
//アプリ起動時に実行される
void initState() {
super.initState();
_initSpeech();
}
// アプリ終了時に実行される
void dispose() {
disposeReply();
super.dispose();
}
// speechToTextを初期化する
void _initSpeech() async {
await _speechToText.initialize();
}
/// 音声認識が開始されると呼び出される
void _startListening() async {
//TODO: speechtotextnotinitializedexceptionのキャッチ
await _speechToText.listen(
onResult: _onSpeechResult, // 単語が認識されると呼び出される
pauseFor: const Duration(seconds: 3), // 何秒音声が聞こえなかったら終了するかを設定
localeId: "en_US", // 認識言語を英語(アメリカ)にする
);
}
// 音声認識の終了ボタンが押されると呼び出される
void _stopListening() async {
await _speechToText.stop();
}
// 認識した単語を処理する
void _onSpeechResult(SpeechRecognitionResult result) async {
// 最終結果が取得できた場合
if (result.finalResult) {
// 認識した単語を取得
_lastWords = result.recognizedWords;
// 何かしらの単語を取得した場合
if (_lastWords != '') {
// 自分の発言としてチャットを表示
setState(() {
_bubbles.add(
Bubbles(_lastWords, true),
);
});
// 自動返信を作成する
final reply = await createReply(_lastWords);
// 相手の発言としてチャットを表示
setState(() {
_bubbles.add(Bubbles(reply, false));
});
}
}
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color.fromARGB(255, 255, 255, 255), //背景色
//タイトルバー
appBar: AppBar(
title: const Text('English Practice'),
backgroundColor: const Color(0xFF96B6C5),
),
body: ListView.builder(
//生成する個数を指定する
itemCount: _bubbles.length,
// ↓itemBuilderの関数型の定義 Widget型を返す
// ここで再描画用のBubbleSpecialThreeをbubblesの個数だけ作成する
itemBuilder: (BuildContext context, int index) {
// 自分の発言の場合、青背景に白文字
if (_bubbles[index].isSender) {
return BubbleSpecialThree(
text: _bubbles[index].message,
isSender: _bubbles[index].isSender,
color: const Color(0xFF96B6C5),
tail: true,
textStyle:
const TextStyle(color: Color(0xFFF4F2DE), fontSize: 20),
);
}
// 自分以外の発言の場合、白背景に青文字
else {
return BubbleSpecialThree(
text: _bubbles[index].message,
isSender: _bubbles[index].isSender,
color: const Color(0xFFF4F2DE),
tail: true,
textStyle:
const TextStyle(color: Color(0xFF96B6C5), fontSize: 20),
);
}
},
),
floatingActionButton: FloatingActionButton(
backgroundColor: const Color(0xFFE9B384),
//マイクボタンが押されたとき、Listen状態でなければstartListeninig、そうでなければstopListeningする
onPressed:
_speechToText.isNotListening ? _startListening : _stopListening,
child: Icon(_speechToText.isNotListening ? Icons.mic_off : Icons.mic),
));
}
}
reply.dart
androidで音声認識を使用するための設定。
reply.dart
import 'package:google_mlkit_smart_reply/google_mlkit_smart_reply.dart';
final SmartReply _smartReply = SmartReply();
void disposeReply() {
_smartReply.close();
}
// 自動返信メッセージを作成
Future<String> createReply(String myMessage) async {
// ユーザから送られたメッセージを追加する
_smartReply.addMessageToConversationFromRemoteUser(
myMessage, DateTime.now().millisecondsSinceEpoch, 'ai');
// 返信候補を取得
final result = await _smartReply.suggestReplies();
// 大きさ3のリストで返ってくるので、とりあえず先頭だけを取得
String reply = result.suggestions[0];
// ユーザに送られたメッセージを追加する
_smartReply.addMessageToConversationFromLocalUser(
reply, DateTime.now().millisecondsSinceEpoch);
return reply;
}
次すること
機能としては完成!
次はapkファイルを作って配布できるようにする
数人かとアプリを共有するためにリリースしたかったのだが、Playストアで共有するためには、内部アプリ共有でも25ドルかかる。
今後、あまりアプリを作って公開する予定は無いので、25ドルをケチって、apkファイルで共有することにした。
1. アイコンの設定
このリンク先の説明を参考に、以下を行う。
- アイコン設定
- デバッグ用バナー削除
2. 証明書を作成する
- 以下コマンドをコンソールに入力する。指定した出力先パスにjksファイルが出力される。
keytool -genkey -v -keystore {出力先のパス}\upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload
- 以下のような情報を聞かれる画面が出てくるので、何も入力せずにEnterを押す。最後の確認のみ
y
を入力。
※Playストア等に公開する場合はちゃんと入力した方が良いと思います。
キーストアのパスワードを入力してください:{パスワードを入力}
新規パスワードを再入力してください:{上と同じパスワードを入力}
姓名は何ですか。
[Unknown]: {Enter}
組織単位名は何ですか。
[Unknown]: {Enter}
組織名は何ですか。
[Unknown]: {Enter}
都市名または地域名は何ですか。
[Unknown]: {Enter}
都道府県名または州名は何ですか。
[Unknown]: {Enter}
この単位に該当する2文字の国コードは何ですか。
[Unknown]: {Enter}
CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknownでよろしいですか。
[いいえ]: y
10,000日間有効な2,048ビットのRSAのキー・ペアと自己署名型証明書(SHA256withRSA)を生成しています
ディレクトリ名: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown
<key>のキー・パスワードを入力してください
(キーストアのパスワードと同じ場合はRETURNを押してください):{ENTER}
- 作成されたupload-keystore.jksは、
android/app/upload-keystore.jks
に置く。
3. プロパティファイルの作成
以下のファイルを作成し、android
直下に置く。
パスワードの部分には、どちらも証明書作成の際に使用したパスワードを入力する。
keyAliasには、証明書の作成に用いたコマンドの最後、-alias <値>
の値を入力(今回の場合upload)。
storePassword={pass}
keyPassword={pass}
keyAlias=upload
storeFile=./upload-keystore.jks
4. build.gradleの設定
android/app
のbuild.gradleを以下のように編集。
android直下のbuild.gradleではないので注意。
//先頭に記載
+def keystoreProperties = new Properties()
+def keystorePropertiesFile = rootProject.file('key.properties')
+if (keystorePropertiesFile.exists()) {
+ keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
+}
/* 中略 */
android {
/* 中略 */
+ signingConfigs {
+ release {
+ keyAlias keystoreProperties['keyAlias']
+ keyPassword keystoreProperties['keyPassword']
+ storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
+ storePassword keystoreProperties['storePassword']
+ }
+ }
buildTypes {
release {
- signingConfig signingConfigs.debug
+ signingConfig signingConfigs.release
}
}
/* 中略 */
}
5. アプリ名の設定
android/app/src/main/AndroidManifest.xml
を編集して、アプリの名前を変更する。
<!-- 関連する部分のみ抜粋 -->
<application
- android:label="{アプリ作成時のデフォルトのプロジェクト名}"
+ android:label="{アプリ名}"
6. APKを構築する
プロジェクトフォルダで、以下のコマンドを実行
flutter build apk --split-per-abi
上手くいけば、以下の三つが作成される。
- [project]/build/app/outputs/apk/release/app-armeabi-v7a-release.apk
- [project]/build/app/outputs/apk/release/app-arm64-v8a-release.apk
- [project]/build/app/outputs/apk/release/app-x86_64-release.apk
7. APKをAndroidにインストールする
様々な方法があるが、今回はGoogle Drive経由で配布した。
方法については、リンク先が詳しいので省略。
補足としては、
- androidだと基本的にarmと書かれているapkファイル二つのどちらかでインストールできる
- インストール画面で、セキュリティ関係の警告が出る場合があるので、その場合無視してインストールボタンを押す(詳細を表示したらインストールボタンが出てくるパターンもある)
- セキュリティ的には微妙なので、本当に顔見知りにちょっと共有したいときじゃなければオススメしない。
参考文献
- Build and release an Android app
- リリースの準備
- Google Play(Androidアプリ)
- Google Play 以外の Android アプリの配信方法(インストールとアップデート)を試してみる
以上でインストールまで完了です。
気力があれば、リリース過程で遭遇したエラーとその対処法についてもまとめたい。
jksファイルの位置が違う
出てきたエラー
PS {プロジェクト場所}\english_practice> flutter build apk --split-per-abi
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:validateSigningRelease'.
> Keystore file '{プロジェクト場所}\english_practice\android\app\{.\upload-keystore.jks}' not found for signing config 'release'.
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
* Get more help at https://help.gradle.org
BUILD FAILED in 4s
Running Gradle task 'assembleRelease'... 5.3s
Gradle task assembleRelease failed with exit code 1
原因
- jskファイルが
android\app
直下に無い
jskファイルの内容が違う
エラー内容
PS {プロジェクト場所}\english_practice> flutter build apk --split-per-abi
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:packageRelease'.
> Multiple task action failures occurred:
> A failure occurred while executing com.android.build.gradle.tasks.PackageAndroidArtifact$IncrementalSplitterRunnable
> com.android.ide.common.signing.KeytoolException: Failed to read key key from store "{プロジェクト場所}\english_practice\android\app\upload-keystore.jks": No key with alias 'key' found in keystore {プロジェクト場所}\english_practice\android\app\upload-keystore.jks
> A failure occurred while executing com.android.build.gradle.tasks.PackageAndroidArtifact$IncrementalSplitterRunnable
> com.android.ide.common.signing.KeytoolException: Failed to read key key from store "{プロジェクト場所}\english_practice\android\app\upload-keystore.jks": No key with alias 'key' found in keystore {プロジェクト場所}\english_practice\android\app\upload-keystore.jks
> A failure occurred while executing com.android.build.gradle.tasks.PackageAndroidArtifact$IncrementalSplitterRunnable
> com.android.ide.common.signing.KeytoolException: Failed to read key key from store "{プロジェクト場所}\english_practice\android\app\upload-keystore.jks": No key with alias 'key' found in keystore {プロジェクト場所}\english_practice\android\app\upload-keystore.jks
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
* Get more help at https://help.gradle.org
BUILD FAILED in 35s
Running Gradle task 'assembleRelease'... 36.2s
Gradle task assembleRelease failed with exit code 1
原因
- jskファイルの中の
keyAlias
の値が、jskファイル生成コマンドの-alias <値>
の値と一致しない