20200519のAndroidに関する記事は4件です。

Android RoomのautoGenerateを安全に使う一つの手法の提案

実行環境

  • AndroidStudio 4.1Preview
  • Kotlin 1.3.72
  • Room 1.1.1

問題提起

AndroidでRoomを使う際のEntityを作成する際のautoGenerateの実装として、以下のような実装を以前までしていました。
(それまでautoGenerateの使い方とかわかってなかったりしました...)

HogeEntity.kt
@Entity(tableName = "hoge")
data class HogeEntity(
    @PrimaryKey(autoGenerate = true)
    val id = 0,
    @ColumnInfo(name = "hoge_title")
    val title: String = ""
)

このようなコードだと、idの値をInsertに使用するEntityを作成する際にidを触ることができてしまう問題が発生します。
今までは「いや、id指定してもRoomがやってくれるんでしょ?」と考えていました。
しかしながら、autoGenerateは以下の時に動作します。

If the field type is long or int (or its TypeConverter converts it to a long or int), Insert methods treat 0 as not-set while inserting the item.

If the field's type is Integer or Long (or its TypeConverter converts it to an Integer or a Long), Insert methods treat null as not-set while inserting the item.

つまり、longまたはintのときには0,IntegerまたはLongのときはnullがデータセットされてない値として判断されて、autoGenerateが動作する模様。
となると、「いや、id指定してもRoomがやってくれるんでしょ?」ということが間違いであると言えます。
(毎回intやlongなら0,IntegerやLongならnullをセットしていていたなら別として...)

だからといって、別のクラスを作って入力データを保持して、Mapperで0やnullを指定してあげたりしてクラス作成時に0というマジックナンバーに見えそうな値を渡すのは、後々苦労しそうです。
さらに、HogeEntityを作る際のidの根本的な触れるという問題は解決できていない。
そこで、以下の実装を提案したいと考えます。

提案するコード

HogeEntity.kt
@Entity(tableName = "hoge")
data class HogeEntity(
    @ColumnInfo(name = "hoge_title")
    val title: String = ""
) {
    @PrimaryKey(autoGenerate = true)
    var id: Int = 0
        get() = field  // <- ここ get() = idにしたらダメだよ!循環参照!(自戒)
}

これで何が嬉しいかと言いますと、DataBaseに登録するのに必要な条件を満たしながら、idを外側から触れないという点にあります。
外側にはgetterしか宣言しておらず、宣言の時にidを指定することができません。
なので、DataBaseに渡るidは0であることが確定するので、autoGenerateが動く保証がされたということで、綺麗に安全にautoGenerateを使うことができていると言えます。

getterしかないのにDataBaseどうやってid振り分けてるの?という疑問

正確な検証は申し訳ありませんが、現時点ではできていません。
しかし、自分が試せる範囲で検証しているので、そこまでの報告をいたします。

では、生成されたJavaコードを見てみると、getterは定義されているのですが、本来privateで生成されているはずのsetterがありません。
そこで、idに一細工を加えます。

HogeEntity.kt
@Entity(tableName = "hoge")
data class HogeEntity(
    @ColumnInfo(name = "hoge_title")
    val title: String = ""
) {
    @PrimaryKey(autoGenerate = true)
    var id: Int = 0
        get() = field
        private set(value) { field = value } //<-ここ入れてみた
}

外部からset()を呼び出せないようにしてみました。
エラーを吐かれましたね!
これで、Javaコードに落とした後に、setterを呼び出してautoGenerateの値を入れていると高確率で考えられます!

まとめ

今回提案したコードでは、Kotlinのdata classとしての状態でidを外部から触るのはできないと思われる(黒魔術的な技があれば可能(?))ので、data class内の初期値さえ管理しておけば、autoGenerateが動く保証がされているということになり、より安全なコードがかけるのではないでしょうか!
もっといいやり方あるよ!とかここ間違ってるよ!といった部分がありましたら、ぜひ教えていただけると幸いです!
(@PrimaryKey(autoGenerate = true)のコードも読んでいきたい)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android RoomのautoGenerateを楽に使う一つの手法の提案

実行環境

  • AndroidStudio 4.1Preview
  • Kotlin 1.3.72
  • Room 1.1.1

問題提起

AndroidでRoomを使う際のEntityを作成する際のautoGenerateの実装として、以下のような実装を以前までしていました。
(それまでautoGenerateの使い方とかわかってなかったりしました...)

HogeEntity.kt
@Entity(tableName = "hoge")
data class HogeEntity(
    @PrimaryKey(autoGenerate = true)
    val id = 0,
    @ColumnInfo(name = "hoge_title")
    val title: String = ""
)

このようなコードだと、idの値をInsertに使用するEntityを作成する際にidを触ることができてしまう問題が発生します。
今までは「いや、id指定してもRoomがやってくれるんでしょ?」と考えていました。
しかしながら、autoGenerateは以下の時に動作します。

If the field type is long or int (or its TypeConverter converts it to a long or int), Insert methods treat 0 as not-set while inserting the item.

If the field's type is Integer or Long (or its TypeConverter converts it to an Integer or a Long), Insert methods treat null as not-set while inserting the item.

つまり、longまたはintのときには0,IntegerまたはLongのときはnullがデータセットされてない値として判断されて、autoGenerateが動作する模様。
となると、「いや、id指定してもRoomがやってくれるんでしょ?」ということが間違いであると言えます。
(毎回intやlongなら0,IntegerやLongならnullをセットしていていたなら別として...)

だからといって、別のクラスを作って入力データを保持して、Mapperで0やnullを指定してあげたりしてクラス作成時に0というマジックナンバーに見えそうな値を渡すのは、後々苦労しそうです。
さらに、HogeEntityを作る際のidの根本的な触れるという問題は解決できていない。
そこで、以下の実装を提案したいと考えます。

提案するコード

HogeEntity.kt
@Entity(tableName = "hoge")
data class HogeEntity(
    @ColumnInfo(name = "hoge_title")
    val title: String = ""
) {
    @PrimaryKey(autoGenerate = true)
    var id: Int = 0
}

これで何が嬉しいかと言いますと、初期値としてidを渡さなくて良いという点です。
変数に格納して、そこからidを指定することが可能なので、完全に安全とは言えないです><

DataBaseはどうやってid振り分けてるの?という疑問

正確な検証は申し訳ありませんが、現時点ではできていません。
しかし、自分が試せる範囲で検証しているので、そこまでの報告をいたします。

では、生成されたJavaコードを見てみると、getterは定義されているのですが、本来privateで生成されているはずのsetterがありません。
そこで、idに一細工を加えます。

HogeEntity.kt
@Entity(tableName = "hoge")
data class HogeEntity(
    @ColumnInfo(name = "hoge_title")
    val title: String = ""
) {
    @PrimaryKey(autoGenerate = true)
    var id: Int = 0
        private set(value) { field = value } //<-ここ入れてみた
}

外部からset()を呼び出せないようにしてみました。
エラーを吐かれましたね!
これで、Javaコードに落とした後に、setterを呼び出してautoGenerateの値を入れていると高確率で考えられます!

まとめ

今回提案したコードでは、Kotlinのdata classとしての状態でidを外部から触るのはできないと思われる(黒魔術的な技があれば可能(?))ので、data class内の初期値さえ管理しておけば、autoGenerateが動く保証がされているということになり、より安全なコードがかけるのではないでしょうか!
もっといいやり方あるよ!とかここ間違ってるよ!といった部分がありましたら、ぜひ教えていただけると幸いです!
(@PrimaryKey(autoGenerate = true)のコードも読んでいきたい)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

眺めて覚える C# Xamarin Forms(14) 連絡先 参照

今回は、連絡先のアクセスがテーマです。

連絡先をアプリに読み込む締めには、許可が必要になります。

image.png

プロジェクト作成の手順

image.png

Propertiesに変更を加えます。

image.png

必要があれば書き込み許可を与えます。

マニフェストには、下記のように自動的に追加されます。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.projectcontact" android:installLocation="auto">
    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
    <application android:label="ProjectContact.Android"></application>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
</manifest>

MainActivity.csに許可のためのルーチンを追加します。①②

MainActivity.cs
using System;

using Android.App;
using Android.Content.PM;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.OS;
//許可ダイアログ用①
using Android.Support.V4.Content;
using Android.Support.V4.App;
using Android;

namespace ProjectContact.Droid
{
    [Activity(Label = "ProjectContact", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;

            base.OnCreate(savedInstanceState);
            // 許可ダイアログのために追加②
            if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.ReadContacts) != (int)Permission.Granted)
                ActivityCompat.RequestPermissions(this, new string[] { Manifest.Permission.ReadContacts }, 0);
            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
            LoadApplication(new App());
        }
        public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
        {
            Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);

            base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }
}

依存関係にNugetで追加します。

image.png

Xamarin.Forms.Contactsを追加します。

image.png

以下の例は、xamlを用いずプログラム的にデザインを作成しています。

MainPage.xaml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace ProjectContact
{
    // Learn more about making custom code visible in the Xamarin.Forms previewer
    // by visiting https://aka.ms/xamarinforms-previewer
    [DesignTimeVisible(false)]
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
#pragma warning disable CS4014 // この呼び出しは待機されなかったため、現在のメソッドの実行は呼び出しの完了を待たずに続行されます
            GetContacs();
        }
        class MyContact
        {
            public string Name { get; set; } = "";
            public string Email { get; set; } = "";
            public string Number { get; set; } = "";
        }
        async Task GetContacs()
        {
            var contacts = await Plugin.ContactService.CrossContactService.Current.GetContactListAsync();
            var list = new List<MyContact>();
            foreach (var x in contacts)
            {
                list.Add(new MyContact
                {
                    Name = x.Name ?? "",
                    Email = x.Email ?? "",
                    Number = (x.Numbers.Count > 0) ? x.Numbers[0] : ""
                });
            }
            var tmp = new DataTemplate(() =>
            {
                var grid = new Grid() { Margin = 0, BackgroundColor = Color.Black };
                grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
                grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
                grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
                grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
                var name = new Label { TextColor = Color.White, FontSize = 18 };
                name.SetBinding(Label.TextProperty, "Name");
                var email = new Label() { TextColor = Color.White };
                email.SetBinding(Label.TextProperty, "Email");
                var number = new Label() { TextColor = Color.White };
                number.SetBinding(Label.TextProperty, "Number");
                grid.Children.Add(name, 0, 0);
                grid.Children.Add(number, 0, 1);
                grid.Children.Add(email, 1, 1);
                return new ViewCell { View = grid };
            });
            var lv = new ListView() { ItemsSource = list, ItemTemplate = tmp };
            Content = new Xamarin.Forms.ScrollView() { Margin = 1, Orientation = ScrollOrientation.Vertical, Content = lv };
        }

    }
}

実行結果

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ステータスバーの透過方法

ステータスバーの透過方法

ステータスバーの透過方法で躓いたので備忘録として残しておきます
早速ですが記述しなければいけないのは以下の二つのみです

styles.xml
 <item name="android:statusBarColor">@android:color/transparent</item>
MainActivity.kt
 window.apply {
            decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
        }

スタイルに書くのは透明色指定のみで大丈夫です
ここで

styles.xml
<style name="android:windowTranslucentStatus">true</item>

と定義してしまうと完全透明ではなく半透明になってしまうので気を付けましょう
次はMainActivityの処理です
他サイトだと

MainActivity.kt
        findViewById<View?>(R.id.content)?.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE

と定義していますが自分ではこれで透過された判定になりませんでした

MainActivity.kt
SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

これはステータスバーが透過された場合にビューが崩れてしまわないように設定しているものです

MainActivity.kt
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR

こちらはステータスバーの文字色を変更する役目を持っています
デフォルトは白色ですがこのコードを挿入してあげると濃いグレーになります

最後に

自分が詰まっているときに検索をかけていたところ様々なところで詰まっている方を見かけたので参考程度になると嬉しいです
ナビゲーションバー等の透過方法が気になる方がいる場合はそちらも書いていこうと思います

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む