モノトーンの伝説日記

Apex Legends, Splatoon, Programming, and so on...

Xamarin.Android を使って,C# と相互運用しないで良い部分を Java で書く。

 おはようございます,変態のモノトーンです。

 今日は変態による変態のための変態講座,その名も「Xamarin.Android で C# に依存していない部分を Java に置き換えて,マーシャリングとかの様々なコストを削減する方法」についてまとめたいと思います。

 そうです,モノトーンさん,やはり変態さんなので,こういうことをやってしまいます。実際問題,Xamarin.Android で native なシステムを使って実装していると,C# を使う部分ってのはいわゆる UI を表示する部分,つまり末端のクラスのみ Java-C# クラスにする,ということができます。これによって微力ながらパフォーマンスアップを測れるかも?

1. MainActivity という上層

 Activity はもはや初期の基本的なルート要素でデータを使った相互運用は基本的に行わないでしょう。例えば,以下の C# コード。

using Android.App;
using Android.OS;
using Android.Runtime;
using Android.Support.Design.Widget;
using Android.Support.V7.App;
using Android.Views;

namespace Mntone.Test.Views
{
    [Register("mntone.test.views.MainActivity")]
    [Activity(Label = "@string/app_name", MainLauncher = true)]
    public sealed class MainActivity : AppCompatActivity, BottomNavigationView.IOnNavigationItemSelectedListener
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            SetContentView(Resource.Layout.activity_main);

            using (var bottomNavigationView = FindViewById<BottomNavigationView>(Resource.Id.bottomNavigationView))
            {
                bottomNavigationView.SetOnNavigationItemSelectedListener(this);
            }
        }

        public bool OnNavigationItemSelected(IMenuItem item)
        {
            Android.Support.V4.App.Fragment fragment;
            switch (item.ItemId)
            {
                case Resource.Id.main_nav_item_testA:
                    fragment = new TestA.TestAFragment();
                    break;

                case Resource.Id.main_nav_item_testB:
                    fragment = new TestB.TestBFragment();
                    break;

                default:
                    return false;
            }
            using (var transaction = SupportFragmentManager.BeginTransaction()
                .Replace(Android.Resource.Id.TabHost, fragment))
            {
                transaction.Commit();
            }
            return true;
        }
    }
}

 Bottom Navigation Bar と Fragment を使うというシンプルなものです。こう言ったコードは Java にこのようなコードが生成されます。

package mntone.test.views;


public class MainActivity
    extends android.support.v7.app.AppCompatActivity
    implements
        mono.android.IGCUserPeer,
        android.support.design.widget.BottomNavigationView.OnNavigationItemSelectedListener
{
/** @hide */
    public static final String __md_methods;
    static {
        __md_methods = 
            "n_onCreate:(Landroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n" +
            "n_onNavigationItemSelected:(Landroid/view/MenuItem;)Z:GetOnNavigationItemSelected_Landroid_view_MenuItem_Handler:Android.Support.Design.Widget.BottomNavigationView/IOnNavigationItemSelectedListenerInvoker, Xamarin.Android.Support.Design\n" +
            "";
        mono.android.Runtime.register ("Mntone.Test.Views.MainActivity, Mntone.Test", MainActivity.class, __md_methods);
    }


    public MainActivity ()
    {
        super ();
        if (getClass () == MainActivity.class)
            mono.android.TypeManager.Activate ("Mntone.Test.Views.MainActivity, Mntone.Test", "", this, new java.lang.Object[] {  });
    }


    public void onCreate (android.os.Bundle p0)
    {
        n_onCreate (p0);
    }

    private native void n_onCreate (android.os.Bundle p0);


    public boolean onNavigationItemSelected (android.view.MenuItem p0)
    {
        return n_onNavigationItemSelected (p0);
    }

    private native boolean n_onNavigationItemSelected (android.view.MenuItem p0);

    private java.util.ArrayList refList;
    public void monodroidAddReference (java.lang.Object obj)
    {
        if (refList == null)
            refList = new java.util.ArrayList ();
        refList.add (obj);
    }

    public void monodroidClearReferences ()
    {
        if (refList != null)
            refList.clear ();
    }
}

 Mono Android はこのような仕組みで,Java から C# コードを読んでいるわけです。しかし,相互運用する必要がない場合,シンプルに Java だけで完結させれば良いと思いませんか? 実はできます。

2. まずは Java 側の名前空間を定義する

namespace Mntone.Test.Views.TestA
{
    [Register("mntone.test.views.testa.TestAFragment")]
    public sealed class TestAFragment 

 このように Java 側の名前空間を明示的に定義します。こうすれば,Java 側にこの名前空間とクラス名で Binding コードが自動生成されます。

3. あとはこのクラスを使って呼び出すだけ。

 Visual Studio で MainActivity.java を追加し,AndroidJavaSource のビルドアクションを選択します。

f:id:mntone:20180729110741p:plain

package mntone.test.views;

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.BottomNavigationView;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.view.MenuItem;
import yourandroid.packagename.R; // Xamarin.Android のパッケージ名 + ".R" を指定。パッケージ名とルート名前空間が一致する場合は記述不要。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        final BottomNavigationView bottomNavigationMenu = findViewById(R.id.bottomNavigationView);
        bottomNavigationMenu.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                Fragment fragment;
                switch (item.getItemId())
                {
                    case R.id.main_nav_item_testA:
                        fragment = new mntone.test.views.testa.TestAFragment();
                        break;

                    case R.id.main_nav_item_testB:
                        fragment = new mntone.test.views.testb.TestBFragment();
                        break;

                 default:
                        return false;
                }
                getSupportFragmentManager().beginTransaction()
                    .replace(android.R.id.tabhost, fragment)
                    .commit();
                return true;
            }
        });
    }
}

 これで Java で UI コードを完結することができます。C# 側で意識するリソースの破棄 (using や Dispose) をあまり考えずにコードを書けます。

 Xamarin.Android のようにマニフェストに自動で Activity が追加されるわけじゃないので明示的に指定すれば完了です♪

<activity android:label="@string/app_name" android:name="mntone.test.views.MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

まとめ

 UI にのみ使うコードを完全 Java だけでかける方法です。C# との相互運用は,リストや画面などの末端表示にしか使わないと思います。その場合,末端のみ C# で書いて,実際の UI のみのコードは Java で書くという方法も取れます。もちろん,完全にオーバーヘッドを取り除くことはできませんが,C# との相互運用部分を極力減らすことで多少でもパフォーマンスをあげたい場合に有効だと思われます。

 ネタでやってみたんですが,思ったより簡単にできてしまったのでネタにしました。Xamarin.Android のビルドプロセスによりこういうことが可能になっているため,仮に Java コードが生成されなくなるようなときがきたらこれは不可能になってしまいますね。

 PS Flutter のアプリ開発に興味が湧いてきた。思ったより良さそうなんだが?