[MAUI Blazor] Androidアプリをつくりたい
Androidのアプリ向けにMAUI Blazorを使うことにしたがあまりに日本語記事がないので備忘もかねてここに溜めていこうと思う
※参考先のURLがない場合は、ChatGPTとやりとりして得られたものになります
下記画像のヘッダー部分などなどのカラーを変える設定
Platforms\Android\Resources\values\colors.xml
- colorPrimary:DisplayAlertのボタンの色など
アプリアイコンの設定
初期アイコンというかデフォルトは「.NET」の文字が書かれた紫背景の丸いアイコン
これではクソダサいので、アプリのアイコンを設定する
公式嫁
すごく簡単にまとめると、
.csproj
内に下記を設定すればアイコンになる
※VisualStudioからだと、パッケージ名をクリックするとエディタが表示されるよ
<ItemGroup>
<MauiIcon Include="アイコンのパス" />
</ItemGroup>
ちなみに、.csproj
内には、何も設定してない状態だと
<!-- App Icon -->
のコメントがあるから、そこの直下に書くといいのではなかろーか
フォント、スプラッシュ、画像などなど?色々ここで設定を追加できますよー
ストレージの考え方・外部ストレージからの画像表示
- 一般的な内部ストレージ:SDカードではなく本体に保存ができるやつ
- 一般的な外部ストレージ:SDカードなど、外部からぶっさすやつ
なんだけども、アプリ(開発)目線だとこうなる
- 内部ストレージ:アプリもしくはOSにのみアクセスできるストレージ
- 外部ストレージ:すべてのアプリ・ユーザーがアクセスできるストレージ
となる
つまり、MAUI Blazor に置き換えて考えると
-
wwwroot
に保存された image ファイル
⇒これは adb などを使って直接閲覧や書き込みができない
⇒アプリ目線、内部ストレージの中にある
このとき、
<img src="ファイルパス" />
のように書いてやれば、簡単に画像を表示することができる
- 連携などで取得して保存された image ファイル
⇒これは外部からアクセスできる領域に保存される
⇒アプリ目線、外部ストレージの中にある
このとき、内部ストレージの場合のように、パスを書くだけでは画像を表示することができない(つらい)
外部ストレージから参照する場合は、バイト配列として画像を読んでエンコードしたものを src に記載する必要がある。そして権限が必要
例
.razor
<img src="@pointImage" class="img-fluid" style="max-width:auto ;height: 220px " />
.razor.cs
// 画像ファイルのパスを取得
var path = Path.Combine(Android.App.Application.Context.FilesDir.AbsolutePath, "image", $"{Const.PathValue.SYSTEM_DIR}{Const.PathValue.IMAGE_DIR}{imagePath}");
// 画像をバイト配列として読み込む
byte[] imageBytes;
try
{
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read))
{
imageBytes = new byte[stream.Length];
await stream.ReadAsync(imageBytes, 0, imageBytes.Length);
}
// 画像をBase64エンコードしてURLを生成
pointImage = $"data:image/jpeg;base64,{Convert.ToBase64String(imageBytes)}"; // ココ
}
catch (Exception e)
{
DroidTrace.Error(e.Message);
}
そもそも権限まわりについて
WebやWindows開発ではあまり意識しないが、モバイルアプリの場合、必ず権限を付与する行為がある
(はじめていんすこしたアプリで「通知を許可しますか~~?」みたいに聞かれるアレ。)
通知以外にも、ファイルアクセス、バイブレーション・・・などなど、色んな動作に権限設定が必要となる
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
のように、パーミッションを与える必要がある
これだけでは不足していて、はじめて起動したときにポップアップとして権限をユーザーに確認するには、
MainActivity.cs で下記のような設定が必要
MainActivity.cs(Maui C#)
// ファイル書き込みの権限があるかどうかを確認する
if (CheckSelfPermission(Android.Manifest.Permission.ManageExternalStorage) == Permission.Granted)
{
// ファイル書き込みの権限がある場合の処理を追加
// ここでファイル書き込みを行うなどの操作を行います
}
else
{
// ファイル書き込みの権限がない場合の処理を追加
// 権限のリクエストを行います
RequestPermissions(new string[] { Android.Manifest.Permission.ManageExternalStorage }, 1);
RequestPermissions(new string[] { Android.Manifest.Permission.WriteExternalStorage }, 1);
}
ログ出力
今回はSystem.Diagnosticsを使ってログを出力してみる
SQLite
- 単独のアプリケーションとして動作させることが可能
- 非常にこんぱくと!
- アプリと一緒に配布することも可能
- ユーザー/パスワードという概念はない
というのが SQLite ちゃんの特徴
実装
公式嫁
MAUI Blazorで使用したい場合は下記が必要になるのでNuGetから取得しておく
- sqlite-net-pcl(ORM)
- SQLitePCLRaw.provider.dynamic_cdecl(SQLiteファイルを読むのに必要)
ORM公式を読めばわかるけど、このORMは同期・非同期どちらも対応できる
ロックの方法とか、そもそものSQLiteファイルパスの指定についてはMS公式の1つ前のページにのっている
直面した課題
インサート成功しているのにデータが登録できていないときがあった
めちゃくちゃ悩んだ(恥)
最初容量の問題か?と思ったが、SQLiteは容量が原因になることはよっぽどないそう(そりゃ別アプリとかが圧迫してれば端末自体の容量で入らないことはある)
単純にAndroidとPCの接続をやめて動かしたらデータが入り解消を確認。
VisualStudio2022にデバッグモードでつないだままデバッグしていたことによって容量圧迫⇒正常にインサートできず という事象であったと推測した
(とはいえデバッグしながら~~ができない場合があるとするとちょっと不便……)
取得データ0件のばあい・new直後のなかみ
たとえば下記のようなテーブルがあるとする
[Table("Test")]
public class Test
{
[PrimaryKey]
public int TestId { get; set; }
public string TestName { get; set; }
}
ここでSelectしたデータが0件だったとき、
string sql = "なんかてきとーなSelectぶん";
List<Test> list = _database.Query<Test>(sql).ToList();
もしくはnewしたとき
Test test = new();
状態としては下記のようになる
- list.Count -> 0
- test.TestId -> 0
- test.TestName -> null
- test 自体は null ではない
なので、「もしデータを取得できなかった場合~~~」みたいなif文を書きたいときは
if (list.Count == 0)
とか
if (test.TestId == 0 && test.TestName is null)
で書くと幸せになれる
if (test is null)
で書くと 怒 ら れ る
外部アプリからデータを受け取りたい(Intent / BroadcastReceiver)
Intent
特定のアクションをリクエストする時に使用できる、メッセージングオブジェクトのこと
BroadcastReceiver
Android オペレーティング システムまたはアプリケーションによってブロードキャストされるメッセージ (AndroidIntent) にアプリケーションが応答できるようにする Android コンポーネント
ワーカースレッドから値を取得してメインで表示する
Invoke を Android でやりたいんだけど、Invoke は Android に対応しておらずエラー!
⇒ Handler を使う
Java でまずかんがえて、そこから MAUI Blazor に変換していく
今回の目標の処理は、
前述した「BroadcastReceiver」を使って値を取得
⇒とった値を画面に表示する
というもの
これをコードで考えると大きく3ステップに分けられる
① BroadcastReceiver を使って値を取得したら、Handler に渡す
② Handler が値を受け取ったら、画面(メイン)に渡す
③ 画面が値を受け取ったら、画面に値を表示する
長くなりそうなのでまとめて記事にするした
不要なプラットフォームのエラーが出ないようにする
MAUI / MAUI Blazor はマルチプラットフォーム開発ができるフレームワークのため、
「このディレクティブは mac では対応してないよ!!!!エラー!!エラー!!」みたいなのが出てくる
今回は Android だけに焦点を当てて開発しているのでこのようなエラーは邪魔
消し方
.csproj を開いて下記を削除する
- 該当のプラットフォームのターゲットを削除
<TargetFrameworks>net7.0-android;net7.0-ios</TargetFrameworks>
↓
<TargetFrameworks>net7.0-android</TargetFrameworks>
- プラットフォームのサポートされているバージョン情報を削除
<!--<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0-maccatalyst|AnyCPU'">
<Optimize>False</Optimize>
<ApplicationDisplayVersion>1.0.0</ApplicationDisplayVersion>
</PropertyGroup>-->
これですっきり🫰🫰
バックグラウンド定期実行について、WorkManagerをつかっていたけど定期実行されていないことがわかったので、ちょっと長くなりそうなので別スクラップで・・・
音、効果音
C# での開発(Maui、Xamarin)では、MediaPlayer を使うことで音声ファイルを流すことが可能
-
例(ChatGPTに聞いた)
効果音ファイル(例: sound.wav)をAndroidデバイスのストレージに保存します。
効果音ファイルのパスを指定してMediaPlayerで再生します。csharpCopy codeusing Android.Media; // ... // 効果音ファイルのパス string soundFilePath = "/sdcard/sound.wav"; // 効果音の再生 MediaPlayer player = new MediaPlayer(); player.SetDataSource(soundFilePath); player.Prepare(); player.Start();
必要に応じて、再生前にMediaPlayerの設定(ループ再生、音量など)をカスタマイズすることもできます。
バイブレーション
バイブレーションの権限を付与することで動かせる
内部設定
SharedPreferences
⇒Android内の設定ファイルよみかきできるや~つ
⇒アンインストールされるときえるよ
Webでいうセッションみたいな機能かな??
フォーカスを当てるとエラーを吐く
await hogeRef.FocusAsync();
でフォーカスを当てられるんだけど、ページ遷移後の OnInitializedAsync 内で実行しようとするとエラーを吐く
⇒結論としては OnAfterRenderAsync を使えば解決する。使っていたライフサイクルの問題
ライフサイクルをおぼえよう
画面のライフサイクルについて、すっごく簡単にまとめると、おそらく下記のようなイメージ
① まず初めてレンダリングされたときに実行する関数の処理
⇒ここに OnInitializedAsync が含まれる
② DOM を処理(つまり画面の HTML だったりのぶぶん)
③ 最後にレンダリング後の処理
⇒ここに OnAfterRenderAsync が含まれる
つまり、OnInitializedAsync を使うと、画面が出来上がる前にフォーカスをあてようとしてしまう
⇒画面ないしフォーカス先のボタンやら何やら何もない!!!!エラー!!!!!
ということになってしまうわけ
キーボードイベント:input してエンター押した判定すると input の中身がとれない
① input に何か入れる
② エンター押す
③ ブレークポイント 貼ると input から取得できるはずの変数に何も入っていない
④ エンター押す
⑤ 変数に値が入っている
ぬぁんで???
答え:Blazor は入力内容が即時反映されないから
<input> 要素がフォーカスを失うと、そのバインドされたフィールドまたはプロパティが更新されます。
そのため、入力してフォーカスが外れないと、データが更新されない!!!
即時更新する方法
input タグに「@bind:event="oninput"」を入れてあげると即時更新ができるようになる
@bind-value でデータを更新取得している場合は @bind-value:event~~~~ を入れてあげる
APIリクエストを投げつけたい
これは MAUI を参考にすればOK
さっくりまとめ
① System.Net.Http の HttpClient を使う
とくに Nuget する必要はない。たぶん
② 例えば Get したい場合は下記のようなかんじ(URLは適当)
HttpClient httpClient;
string baseUrl = "http://123.456.789.1:8000/hoge";
public async Task GetApi()
{
httpClient = new HttpClient();
try
{
// Get する
var response = await httpClient.GetAsync(baseUrl);
// 成功したら中身を取得する
// ここで JSON をさわりたいなら JSON のライブラリが必要
if (response.IsSuccessStatusCode)
{
string content = await response.Content.ReadAsStringAsync();
}
}
catch(Exception e)
{
Console.WriteLine(e.Message);
}
}
Cleartext HTTP traffic to 10.3.20.111 not permitted
https じゃない場合、Android はデフォルトでは繋げないようにしている(かしこいね)
今回は社内のイントラなのでできれば http につなげたい
解決方法としては、2種類の方法がある
① すべての HTTP に対して許可をする
AndroidManifest の application タグに下記を追加する
android:usesCleartextTraffic="true"
② 指定したアドレスに対して許可をする
network_security_config.xml を作成して例外のアドレスを指定
その後 AndroidManifest に network_security_config を追加する
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">123.456.789.1</domain>
</domain-config>
</network-security-config>
disabled を動的に決める
hoge.razor.cs 側で disabled 扱いたい…扱いたくない?
hoge.razor
<input disabled="@test">
hoge.razor.cs
private bool test = true;
こうすることで test の値が変われば disabled を動的に決めることができる
HTML の class を動的に決める
hoge.razor.cs 側で以下略
hoge.razor
<p class="@(test == 0? "text-primary" : "")" >てすと< /p>
hoge.razor.cs
private int test = 1;
こんなかんじ
動的に決めた文字列で改行も含めたい
MarkupString を使用する
hoge.razor
<p>@((MarkupString)message)< /p>
hoge.razor.cs
private string message = "これは<br>テストです";
※Android
MAUI Blazor では Bootstrap のモーダルが使えない??(未解決)下記リンクのようなコンポーネントにパラメータを与える方法で、モーダルのコンポーネントを作ってしまおうと考えた
で、生み出したのが大体下記リンクの質問者と同じソース
ここで、パラメータを与える方法は成功しているんだけど、
Showにtrueを入れてもモーダルが表示されなかった
うーん、JSの発火のタイミングとかの問題?
下記のようにすればJS読めそう??そしたら解決できるかも???
と思ったが、別の方法で解決させることにしたので試してない
案件おわったのでいったんクローズ。また始まったらオープンする