Closed22

[MAUI Blazor] Androidアプリをつくりたい

あざらしあざらし

Androidのアプリ向けにMAUI Blazorを使うことにしたがあまりに日本語記事がないので備忘もかねてここに溜めていこうと思う
※参考先のURLがない場合は、ChatGPTとやりとりして得られたものになります

あざらしあざらし

下記画像のヘッダー部分などなどのカラーを変える設定

Platforms\Android\Resources\values\colors.xml
  • colorPrimary:DisplayAlertのボタンの色など
あざらしあざらし

アプリアイコンの設定

初期アイコンというかデフォルトは「.NET」の文字が書かれた紫背景の丸いアイコン
これではクソダサいので、アプリのアイコンを設定する

公式嫁
https://learn.microsoft.com/ja-jp/dotnet/maui/user-interface/images/app-icons?tabs=android

すごく簡単にまとめると、
.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);
        }
あざらしあざらし

SQLite

  • 単独のアプリケーションとして動作させることが可能
  • 非常にこんぱくと!
  • アプリと一緒に配布することも可能
  • ユーザー/パスワードという概念はない

というのが SQLite ちゃんの特徴

実装

公式嫁

https://learn.microsoft.com/ja-jp/xamarin/android/data-cloud/data-access/using-sqlite-orm
https://github.com/praeclarum/sqlite-net

MAUI Blazorで使用したい場合は下記が必要になるのでNuGetから取得しておく

  • sqlite-net-pcl(ORM)
  • SQLitePCLRaw.provider.dynamic_cdecl(SQLiteファイルを読むのに必要)

ORM公式を読めばわかるけど、このORMは同期・非同期どちらも対応できる
ロックの方法とか、そもそものSQLiteファイルパスの指定についてはMS公式の1つ前のページにのっている

https://learn.microsoft.com/ja-jp/xamarin/android/data-cloud/data-access/configuration

直面した課題

インサート成功しているのにデータが登録できていないときがあった

めちゃくちゃ悩んだ(恥)

最初容量の問題か?と思ったが、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

特定のアクションをリクエストする時に使用できる、メッセージングオブジェクトのこと

https://techgrowup.net/2021/05/18/android-intent/

BroadcastReceiver

Android オペレーティング システムまたはアプリケーションによってブロードキャストされるメッセージ (AndroidIntent) にアプリケーションが応答できるようにする Android コンポーネント

https://learn.microsoft.com/ja-jp/xamarin/android/app-fundamentals/broadcast-receivers

あざらしあざらし

ワーカースレッドから値を取得してメインで表示する

Invoke を Android でやりたいんだけど、Invoke は Android に対応しておらずエラー!
⇒ Handler を使う
Java でまずかんがえて、そこから MAUI Blazor に変換していく


今回の目標の処理は、
前述した「BroadcastReceiver」を使って値を取得
⇒とった値を画面に表示する
というもの

これをコードで考えると大きく3ステップに分けられる
① BroadcastReceiver を使って値を取得したら、Handler に渡す
② Handler が値を受け取ったら、画面(メイン)に渡す
③ 画面が値を受け取ったら、画面に値を表示する

長くなりそうなのでまとめて記事にするした
https://zenn.dev/azarashi0519/articles/16b75353e615d1

あざらしあざらし

不要なプラットフォームのエラーが出ないようにする

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

https://stackoverflow.com/questions/72633566/how-can-i-remove-the-platforms-i-do-not-need-to-compile-for-in-net-maui

これですっきり🫰🫰

あざらしあざらし

音、効果音

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の設定(ループ再生、音量など)をカスタマイズすることもできます。

バイブレーション

バイブレーションの権限を付与することで動かせる

https://learn.microsoft.com/ja-jp/dotnet/maui/platform-integration/device/vibrate?tabs=windows

あざらしあざらし

フォーカスを当てるとエラーを吐く

await hogeRef.FocusAsync();

でフォーカスを当てられるんだけど、ページ遷移後の OnInitializedAsync 内で実行しようとするとエラーを吐く
⇒結論としては OnAfterRenderAsync を使えば解決する。使っていたライフサイクルの問題

https://qiita.com/jsakamoto/items/1aef0e455870d291b0c8

あざらしあざらし

ライフサイクルをおぼえよう

画面のライフサイクルについて、すっごく簡単にまとめると、おそらく下記のようなイメージ

① まず初めてレンダリングされたときに実行する関数の処理
⇒ここに OnInitializedAsync が含まれる
② DOM を処理(つまり画面の HTML だったりのぶぶん)
③ 最後にレンダリング後の処理
⇒ここに OnAfterRenderAsync が含まれる

つまり、OnInitializedAsync を使うと、画面が出来上がる前にフォーカスをあてようとしてしまう
⇒画面ないしフォーカス先のボタンやら何やら何もない!!!!エラー!!!!!

ということになってしまうわけ

https://learn.microsoft.com/ja-jp/aspnet/core/blazor/components/lifecycle?view=aspnetcore-8.0

あざらしあざらし

キーボードイベント:input してエンター押した判定すると input の中身がとれない

① input に何か入れる
② エンター押す
③ ブレークポイント 貼ると input から取得できるはずの変数に何も入っていない
④ エンター押す
⑤ 変数に値が入っている

ぬぁんで???

答え:Blazor は入力内容が即時反映されないから

<input> 要素がフォーカスを失うと、そのバインドされたフィールドまたはプロパティが更新されます。

そのため、入力してフォーカスが外れないと、データが更新されない!!!

即時更新する方法

input タグに「@bind:event="oninput"」を入れてあげると即時更新ができるようになる
@bind-value でデータを更新取得している場合は @bind-value:event~~~~ を入れてあげる

https://learn.microsoft.com/ja-jp/aspnet/core/blazor/components/data-binding?view=aspnetcore-8.0

https://qiita.com/jsakamoto/items/124065b0cc339eba2ff9

あざらしあざらし

APIリクエストを投げつけたい

これは MAUI を参考にすればOK

https://learn.microsoft.com/ja-jp/dotnet/maui/data-cloud/rest?view=net-maui-8.0

さっくりまとめ

① 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 はデフォルトでは繋げないようにしている(かしこいね)

https://developer.android.com/topic/security/risks/cleartext?hl=ja

今回は社内のイントラなのでできれば 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>

https://codechacha.com/ja/android-cleartext-http-traffic-issue/

あざらしあざらし

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;

こんなかんじ

あざらしあざらし

MAUI Blazor では Bootstrap のモーダルが使えない??(未解決)
※Android

下記リンクのようなコンポーネントにパラメータを与える方法で、モーダルのコンポーネントを作ってしまおうと考えた

https://www.ipentec.com/document/csharp-blazor-application-give-parameter-to-razor-component

で、生み出したのが大体下記リンクの質問者と同じソース

https://stackoverflow.com/questions/59256798/how-to-use-bootstrap-modal-in-blazor-client-app

ここで、パラメータを与える方法は成功しているんだけど、
Showにtrueを入れてもモーダルが表示されなかった

うーん、JSの発火のタイミングとかの問題?

下記のようにすればJS読めそう??そしたら解決できるかも???
と思ったが、別の方法で解決させることにしたので試してない

https://pg-himajin.com/dotnet/maui/drawing-1/

あざらしあざらし

案件おわったのでいったんクローズ。また始まったらオープンする

このスクラップは2ヶ月前にクローズされました