AndroidでのContextの扱いについて

f:id:nomunomu0504:20190411144523p:plain:w0

基本的にAndroidで開発して行く上でContextが必ずといっていいほどつきまとう。Application#getApplicationContextで取得できるContextを簡単かつ安全に使えたらってことで今日の話

ネストが深いところでContextが必要になってくると、それ以前の引数にContextを追加しないといけなくなる。 メンテナンスや改良をする際に厄介になることは言うまでもない。つまり、どこでも簡単にApplication#getApplicationContextで得られるContextを使えたら開発者もハッピーになるわけだ。

まずはContextについて ・Contextには「ActivityのContext」と「ApplicationのContext」の2種類が存在する
- ActivityのContextはよく使われるthis。ApplicationContextはApplication#getApplicationContext()

この2つのContextの違いと問題について ActivityのContextは、Activityのライフサイクルに依存する。そのためActivityが破棄される時にContextも破棄しないとメモリリークが発生する。 Contextが保持されたままだとActivityは論理的に終了(onDestroy)するが、ActivityのObject類は破棄されない。finalize()が呼ばれない状態。finalize()とはClassがメモリも含め終了した時にGC実行時に呼ばれる。finalize()はObjectクラスのメンバ。つまりはGCの対象とならない。 Activityは頻繁に破棄、生成される。画面を回転しただけでもデフォルトの設定だとonDestroyされる。デフォルトではシステムが現在のActivityを破棄しその状態を保持しながら新しいActivityを生成しようとするかららしい。なのでActivityのライフサイクルにContextを依存させないといろいろ厄介。

一方ApplicationContextはアプリケーションに依存する。つまりアプリケーションが生きている限り有効。アプリケーションに依存するクラス、複数のクラスから呼ばれたりする時などに使用するのが望ましい。ダイアログなどのContextにApplicationContextは使用できないので注意。

AlertDialog.Builder builder
                = new AlertDialog.Builder(this.getApplicationContext());  //★
builder.setTitle("test");
ArrayAdapter<String> adapter
                = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
adapter.add("item");
builder.setAdapter(adapter, null);
builder.setPositiveButton("OK", null);

★の部分でこんなエラーが発生する(はず)

E/AndroidRuntime(4578): Caused by: android.view.WindowManager$BadTokenException:
     Unable to add window -- token null is not for an application

まとめると ActivityContextはActivityと共同体 ApplicationContextはApplicationと共同体 です。なんでもかんでもApplicationContextを使えばいいってわけでもないので、よく考えて使いましょう。 ここに詳しく書いてあります

で、話を戻してApplicationContextをどこからでも参照できるようにします

public class AppContext {
   private static AppContext Instance = null;
   private Context ApplicationContext;

   //
   // publicはつけない
   // このメソッドのアクセスレベルはパッケージローカルとする
   // 念のため意図しないところで呼び出されることを防ぐ
   //

   static void onCreateApp(Context ApplicationContext) {
     //
     // Application#onCreateのタイミングでシングルトンが生成される
     //
     Instance = new AppContext(ApplicationContext);
   }

   private void AppContext(Context ApplicationContext) {
     this.ApplicationContext = ApplicationContext;
   }

   public static AppContext getInstance() {
     if (Instance == null) {
        throw new RuntimeException("MyContext should be initialized!");
     }
     return Instance;
   }
}

public class Main_Application extends Application {
    //
    // Application#onCreateは、ActivityやServiceが生成される前に呼ばれる。
    //

   @Override
   public void onCreate() {
       super.onCreate();
       AppContext.onCreateApp(getApplicationContext());
   }
}

こうしておけば

AppContext.getInstance().getApplicationContext();

で参照できる。でも新たにクラスを作るのが嫌だという場合はMain_Applicationクラスの内部でInstanceを保持させる

public class Main_Application extends Application {
    private static Main_Application Instance = null;
    
    @Override
    public void onCreate() {
        super.onCreate();
        Instance = this;
    }

    public static Main_Application getContext() {
        return Instance;
    }
}

こうすれば

Main_Application.getContext().getApplicationContext();
Main_Application.getContext();

実際、Main_ApplicationはContextとしても使えるので、上の2つどちらの書き方でも問題はない。

となると

AppContext.getInstance().getApplicationContext();

よりも

Main_Application.getContext();

のような書き方の方が、コード量も多少ではあるが少なくなる。ということで先ほどのコードを少し修正

AppContext#getContext()を修正します

public static AppContext getContext() {
     if (Instance == null) {
        throw new RuntimeException("MyContext should be initialized!");
     }
     return Instance;
}

から

public static Context getContext() {
     if (Instance == null) {
        throw new RuntimeException("MyContext should be initialized!");
     }
     return Instance.getApplicationContext();
}

こうしておけば

AppContext.getContext()

でApplicationContextを参照できるようになります。

以上、今日はここまで