164
191

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

最近のAndroidネイティブ開発まとめ(2017/4版)

Last updated at Posted at 2017-01-02

2017/04/23更新

  • buildConfigFieldについて追記
  • ライブラリ情報更新
  • 通信処理(RxJava2+RxAndroid+Retrofit2)について追記

はじめに

最近Androidネイティブ開発で消耗しています。
モダンなAndroidネイティブ開発に関してまとめてみました。

基本

AndroidManifest.xml

主に次の項目を設定する

  • アプリの権限の設定
  • ApplicationおよびActivityの初期設定
  • BroadcastReceiverの設定

アプリのパーミッション(権限)を追加したい場合はapplicationタグの前にuser-permissonタグを追記

<uses-permission android:name="android.permission.INTERNET" />

上記は一番よく使う通信を許可するためのパーミッション
name属性値に許可したいパーミッションを指定します。

build.gradle

アプリケーションの次のビルド設定を記載する

  • アプリバージョン
  • ビルド切り替え設定(ProductFlavor)
  • ライブラリ管理
  • Proguard設定
  • 設定値

特にライブラリを追加するときにはdependencies以下に追記

dependencies {
    compile 'ライブラリ'
}

Proguard

Androidパッケージファイルを難読化するためのツール
GooglePlayストアにアップロードされているapkをデコンパイルされても解析されないようにする

bundle.gradleにてProguardファイルを設定
product-project.txtにProguardの設定をします。
(特定のファイルは難読化しないなど)

 buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-project.txt'
            ##proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' ←もとから設定してあった
        }
 }

サーバ接続情報など固定値の設定パラメータは
buildConfigFieldを利用するとビルド時に固定値が埋め込まれます。
さらにbuildTypes以下のdebug、release内に書くことでデバッグ時、リリース時に固定値を切り替えることができます。

build.gradle

    defaultConfig {        
        // 固定値パラメータ(共通)
        buildConfigField 'String', 'END_POINT', '"https://randomuser.me/"'
    }
    buildTypes {
        debug {
            // debug時のFabricなどの認証情報
           buildConfigField 'String', 'KEY', '"xxxx-xxxx-xxxx-xxxx"'
        }
        release {
            // release時のFabricなどの認証情報
           buildConfigField 'String', 'KEY', '"yyyy-yyyy-yyyy-yyyy"'
        }
    }

Javaコード側での固定値の取得は次のようになります。

test.java
String serverUrl = BuildConfig.END_POINT;

Proguardには次のように難読化除外のパッケージ指定を主にする。
(指定しない場合は難読化される)

-keep class com.hannesdorfmann.fragmentargs.** { *; }
-keepnames class jp.misyobun.lib.versionupdater.** { *; }
  • -keep:クラスを難読化しない
  • -keepnames:変数を難読化しない

keepnamesに関してはきちんとやっておかないとGSONのモデルマッピングに失敗したりとハマります。

アプリアイコン・アプリ名

アプリ名はstrings.xmlにてapp_name項目で指定

 <string name="app_name">アプリ名</string>

アプリアイコンはAndroidManifest.xmlの
applicationタグのandroid:iconに設定

 <application
     android:icon="@mipmap/ic_launcher"

テーマ

アプリ全体のデザインを変えるためにはテーマがある
Android一般のHoloやマテリアルデザイン用のMaterialなどがある
AppCompatテーマはAndroid OS5(Lolipop)以前にマテリアルデザインを
割り当てるためのテーマで、アクションバーが不要な場合(後で自作したい場合など)は
次のような設定をstyles.xmlに設定する

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>

colorPrimary:アクションバーの背景色
colorPrimaryDark:ステータスバーの背景色
navigationBarColor:ナビゲーションバーの背景色
windowBackground:アプリケーションボディの背景色
textColorPrimary:アクションバーのタイトル文字色
colorAccent:コントロールの基本色

ThemeColors.png

AndroidManifest.xmlのapplicationタグにテーマが設定されている

<application
        android:theme="@style/AppTheme">

ライフサイクル

Activityのライフサイクル

7264706d-ccd7-b57a-3eca-cdf4c6675a3d.png

Fragmentのライフサイクル

a23fa119-b41b-f7a2-b940-5ca1a79157e4.png

画面設計

AndroidではFragment移動時にはBackStackという領域にFragmentを保存する。
遷移元から2方向以上で循環できるような構造があると厄介なことになる。
TabとかBottom Navigationとかだと一つのActivity内で複数のFragmentを保持するため、この問題が起こりやすい。(タブ内のFragmentの2階層以上深い階層にTabやBottom Navigationが存在する場合)
上記の場合、BackStackをタブ別に持つなどの実装になるため、実装は非常に複雑。
そもそもAndroidの画面遷移構造に適していない。

ちなみにBackボタン(BackStack)とUpボタンの挙動は次のようなガイドラインに沿うのが推奨されています。
Back ボタンと Up ボタンを使用したナビゲーション

navigation_between_siblings_gmail.png

設計・ライブラリ

Context、MultiDex、アプリケーション共通

Context

Contextはアプリケーションの各所で必要になる
ApplicationのContext(getApplicationContext())やActivityのContext(getContext())などがある
正確な違いは不透明だが
ActivityのContextを利用するとライフサイクル終了時に破棄されるため
タイミングによってはNull Pointer Exception(以下NPE)になる
ApplicationのContextを参照するようにしたほうが安全
また、利用するContextによって作られる標準のDialogやListViewコントロールのデザインはAndroidManifest.xmlのThemeに引っ張られる模様
Android:引数はthisか?getApplicationContextか?ActivityとApplicationの違い

Multidex

Androidではdexと呼ばれる方式でパッケージ生成を行う
ただし、1パッケージあたり65kが上限という問題があるため
メソッド数が増えてくると引っかかる可能性が出てくる
MultidexApplicationクラスを継承し、設定することで回避できる

アプリケーション共通

MultiDexApplicationクラスを利用するためには
build.gradleのdepandenciesにmultidexライブラリを追加

dependencies {
    compile 'com.android.support:multidex:1.0.0'
}

MultiDexApplicationクラスを拡張したMainAppliationクラスを作成する

public class MainApplication extends MultiDexApplication {

    private static Context context;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }

    // アプリContext取得
    public static Context getContext(){
        return context;
    }

    @Override
    public void onCreate(){
        super.onCreate();
        context = getApplicationContext();
    }
}

上記自作MainApplicationクラスを呼び出すには
AndroidManifest.xmlに自作MainApplicationクラスを指定

 <application
        android:name=".MainApplication"

ラムダ式

retrolambaを使うと利用できる。
JDK8を使うため、
OracleのサイトからJDK 8をダウンロードする

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath 'me.tatarka:gradle-retrolambda:3.4.0'
    }
}

// Required because retrolambda is on maven central
repositories {
    mavenCentral()
}

apply plugin: 'me.tatarka.retrolambda'

android {
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

ラムダ式導入前

Handler handler = new Handler();
handler.post(new Runnable() {
       @Override
       public void run() {
                
       }
});

ラムダ式導入後

抽象メソッドが一つの場合はメソッド名、引数の表記を省略できる
ネストも減り、大分すっきりします。

Handler handler = new Handler();
handler.post(() -> {
    // 処理
});

通信

OkHttp3+Retrofit2+Gsonが良いです。
GETに関して良い記事が下に
Retrofit2使ってみる

OkHttp3+Retrofit2+Gsonを導入するには次のようにbuild.gradleに記述します。

build.gradle
dependencies{
    // okhttp3+retrofit2+gson
    compile 'com.squareup.okhttp3:okhttp:3.3.1'
    compile 'com.squareup.retrofit2:retrofit:2.2.0'
    compile 'com.squareup.retrofit2:converter-gson:2.2.0'
    compile 'com.squareup.okhttp3:logging-interceptor:3.3.1'
    // GSON
    compile 'com.google.code.gson:gson:2.6.2'
}

POSTのmultipart-formでのファイルアップロードに関してあんまり書かれてなかったりするので追記しておきます。
インタフェースにMultiPartアノテーションをつけます。
パラメータはPartアノテーションをつけます。
ファイルのパラメータはフォーマットに注意です。
name.jpgが送信ファイル名になります。

public interface Api {

        @Multipart
        @POST("/api/files.upload")
        Call<Void> uploadFile(@Part("param") RequestBody param,
                              @Part("file\"; filename=name.jpg") RequestBody file // file
                );

}

次のように呼び出し

// インタフェース生成
Api api = new Retrofit.Builder()
                .baseUrl("http://xxx")
                .build()
                .create(Api.class);

// 画像ファイル取得
File file = new File("画像パス");

// RequestBodyで送信パラメータ作成
// MediaTypeでMIME指定
Call<Void> request = api.uploadFile(RequestBody.create(MediaType.parse("plain/text"), "パラメータ"),
                RequestBody.create(MediaType.parse("image/jpeg"), file));

request.enqueue(new Callback<Void>() {
            @Override
            public void onResponse(Call<Void> call, Response<Void> response) {
                Log.d("SendRetrofit",response.toString());

            }

            @Override
            public void onFailure(Call<Void> call, Throwable t) {
                Log.e("ErrorRetrofit",t.toString());

            }
});

最近では、RxJava2+RxAndroidと組み合わせることで
複数の通信処理(非同期処理)を順次的に処理させる記述が楽になります。
さらに後述のData Bindingsと組み合わせるとMVVMでの実装が可能になります。
参考:Handling API calls using Retrofit 2 and RxJava 2

RxJava2、RxAndroidを導入するには次のようにします。

build.gradle
dependencies{
    // RxJava2+RxAndroid
    compile "io.reactivex.rxjava2:rxjava:2.0.9"
    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
    // Retrofit2のRxJava2プラグイン
    compile 'com.squareup.retrofit2:adapter-rxjava2:2.2.0'
}

ランダムにユーザを生成するサービスのAPIからデータ取得してみます。
RANDOM USER GENERATOR

Retrofit2用のインタフェースを作成します。
Callではなく、Observableで戻り値を定義していることに注意してください

UserApiClient.java
interface UserApiClient {
   // ランダムユーザ取得API (https://randomuser.me/api)
   @GET("api")
   Observable<JsonObject> getUser();
}

OkHttp3、Gson、Retrofit2のインスタンスを生成し、
API呼び出し&データ取得部分に関しては次のようになります。
Retrofit2生成までは生成処理が重いため、一度生成したら使いまわした方が無難です。

User.java
public void getUser(){
   // OkHttp3 Client生成
   OkHttpClient.Builder builder = new OkHttpClient.Builder().addInterceptor(
                new Interceptor() {
                    @Override
                    public Response intercept(Interceptor.Chain chain) throws IOException {
                        Request original = chain.request();

                        // リクエスト共通パラメータ追加
                        /*
                        HttpUrl url = original.url().newBuilder()
                                .addQueryParameter("common_param","hogehoge").build();

                        Request request = original.newBuilder().url(url).build();
                        return chain.proceed(request);
                        */
                        return chain.proceed(original);
                    }
                });
        // 読み込みタイムアウト
        builder.readTimeout(3000, TimeUnit.MILLISECONDS);
        // 書き込みタイムアウト
        builder.writeTimeout(3000, TimeUnit.MILLISECONDS);
        // 接続タイムアウト
        builder.connectTimeout(3000,TimeUnit.MILLISECONDS);
   OkHttpClient okHttpClient = builder.build();
   // Gson生成
   Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
   // Retrofit2生成
   Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://randomuser.me")
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build();
   // Reterofit2のAPIインタフェースを生成し、通信処理
   UserApiClient userApiClient = retrofit.create(UserApiClient.class);
   userApiClient.getUser() // APIコール
                .subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread()) // UIスレッドに渡す
                .subscribe(response -> {
                    Log.d("param",response.toString()); // 成功
                },throwable -> {
                    Log.d("error:",throwable.toString()); // エラー
                });
}

retrolambdaを使わない場合(Java8を入れたくない場合)は
下記リンクの関数型インタフェースを参考にしてください
RxJava2での変更点

上記の例だと次のように書き直せます

User.java
userApiClient.getUser()
                .subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread()) // UIスレッドに渡す
                .subscribe(new Consumer<JsonObject>() {
                    @Override
                    public void accept(@NonNull JsonObject response) throws Exception {
                        Log.d("param",response.toString());
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(@NonNull Throwable throwable) throws Exception {
                        Log.d("error:",throwable.toString());
                    }
                });

NPE対策

RxJava2,RxAndroidのOptinalを使うとnullかもしれないオブジェクトの処理を明示的にできる
build.gradleにライブラリを追加

dependencies{
    compile "io.reactivex.rxjava2:rxjava:2.0.9"
    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
}

Optinal導入前

String strNullable = "456";
int result = 123;
if(strNullable != null ){
    result = Integer.valueOf(strNullable);
}

Optinal導入後

Optinalを使った記述だと
Nullの場合の記述を明示的にできるし
煩雑なif文やfor文が消え去る

String strNullable = "456";
int result = Optional.ofNullable(strNullable)
             .map(Integer::valueOf)
             .orElse(123); // if null

Fragment引数のDI

FragmentArgsを使うとIntentの引数をアノテーションでinjectできるようになる。
build.gradleにてインストール

build.gradle

dependencies{
    compile 'com.hannesdorfmann.fragmentargs:annotation:3.0.2'
    annotationProcessor 'com.hannesdorfmann.fragmentargs:processor:3.0.2'
    compile 'com.hannesdorfmann.fragmentargs:bundler-parceler:3.0.2'
}

MainFragment.javaを作成します。
引数を受け取りたいFragmentにFragmentWithArgsアノテーションをつけます
受け取る引数にはArgアノテーションをつけます。
onCreateにて「Fragmentクラス名Builder」.injectArguments(this)を呼び出します。
required = falseにすることでオプションパラメータにもできます。

@FragmentWithArgs
public class MainFragment extends Fragment{

    @Arg(required = false)
    String age;
    @Arg
    String name;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FragmentArgs.inject(this); // read @Arg fields
    }

    // Fragmentで表示するViewを作成するメソッド
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        View view = inflater.inflate(R.layout.fragment_main, container, false);

        TextView nameText = (TextView) view.findViewById(R.id.name);
        nameText.setText(name);
        TextView ageText = (TextView) view.findViewById(R.id.age);
        ageText.setText(age);

        return view;
    }

}

MainActivity.javaからは次のように呼び出し
MainFragmentBuilderはビルド時に自動生成されます。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // ビルダーのコンストラクタ引数の順番は変数のアルファベット順になるので注意
        MainFragment mainFragment = new MainFragmentBuilder("名前")
                .age("年齢") // オプションパラメータ
                .build();
        getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.container, mainFragment,MainFragment.class.getName())
                .commit();

    }
}

Parcelerでのモデルクラスシリアライズ

クラスオブジェクトを引数に渡す際はParcelインタフェース実装などでシリアライズが必要です。
Parcelerライブラリのアノテーションを使うとParcelインタフェース実装に必要な煩雑な記述が不要になります。
Parcelerをインストールします。
build.gradleに追記


dependencies{
    // Parceler
    compile 'org.parceler:parceler-api:1.1.6'
    annotationProcessor 'org.parceler:parceler:1.1.6'
}

Userモデルを定義してみます。
次のParcel化するモデルに関してはParcelアノテーションをつけるだけで大丈夫です。

import org.parceler.Parcel;

@Parcel
public class User {

    String name;
    int age;

    public User() {}

    public User(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public String getName() { return name; }

    public int getAge() { return age; }
}

Fragment側ではParcelオブジェクトに関しては
bundler = ParcelerArgsBundler.classを設定します。

@FragmentWithArgs
public class MainFragment extends Fragment{

    @Arg( bundler = ParcelerArgsBundler.class )
    User user;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FragmentArgs.inject(this); // read @Arg fields
    }

    // Fragmentで表示するViewを作成するメソッド
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        View view = inflater.inflate(R.layout.fragment_main, container, false);

        TextView nameText = (TextView) view.findViewById(R.id.name);
        nameText.setText(user.getName());
        TextView ageText = (TextView) view.findViewById(R.id.age);
        ageText.setText(String.format("%dさい",user.getAge()));

        return view;
    }
}

MainActivityではUserモデルを生成して引数に渡します。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 引数の順番は変数のアルファベット順になるので注意
        MainFragment mainFragment = new MainFragmentBuilder(new User(15,"名前"))
                .build();
        getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.container, mainFragment,MainFragment.class.getName())
                .commit();

    }
}

ライフサイクル時の変数保存

ライフサイクルで厄介なのがActivityはすぐ死ぬということ
特にActivityの回転を許している場合(デフォルト)
回転するだけでActivityが死んで再表示時にonCreateから再生成される。
この場合、何が厄介かというとEditText入力中に回転などされると
onCreateにてActivityやFragmentのデータが初期化されてしまう。
これを防ぐために、ActivityのonSaveInstanceStateメソッドにて保存しておきたい変数はBundleに保存する。
Icepickというライブラリを使えばもっと楽になる。
build.gradleに追記

dependencies{
    // Icepick
    compile 'frankiesardo:icepick:3.2.0'
    provided 'frankiesardo:icepick-processor:3.2.0'
}

実際にActivityで状態保存して再生成時に取得するには次のようにする。

@State(UserBundler.class) User user;

@Override
public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Icepick.restoreInstanceState(this, savedInstanceState);
        setContentView(R.layout.activity_main);

        if(user == null) {
            user = new User(15, "名前");
        }
        
}

@Override
protected void onSaveInstanceState(Bundle outState) {
     super.onSaveInstanceState(outState);
     Icepick.saveInstanceState(this, outState);
}

Parcelerで直列化したUserモデルをBundlerにマッピングするための
UserBundlerクラスは次のように定義する

import android.os.Bundle;
import org.parceler.Parcels;
import icepick.Bundler;

// Icepick
public class UserBundler implements Bundler<User> {
    @Override
    public void put(String s, User example, Bundle bundle) {
        bundle.putParcelable(s, Parcels.wrap(example));
    }

    @Override
    public User get(String s, Bundle bundle) {
        return Parcels.unwrap(bundle.getParcelable(s));
    }
}

クラス変数のDI(Dependency Injection)

クラスのインスタンス生成を外部から生成できるようにする
主にアプリ内共通モデルデータをDIすると分離が楽になる
Dagger2が一番良いと思います。
aptのインストールはFragmentArgsと同じなのでそちらを参考に
build.gradleでDagger2をインストールします。

{
    compile 'com.google.dagger:dagger:2.9'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.9'
    provided 'javax.annotation:jsr250-api:1.0'
}

インジェクト元のオブジェクトにはModuleアノテーションをつけます。
ここではAppModule.javaを作成します。
インジェクトするオブジェクトを返却するメソッドにはProvidesアノテーションをつけます。

import android.app.Application;
import android.content.Context;
import android.support.multidex.MultiDexApplication;

import javax.inject.Singleton;

import dagger.Module;
import dagger.Provides;

@Module
public class AppModule {
    private final Context appContext;

    public AppModule(Context context) {
        this.appContext = context;
    }

    @Provides
    @Singleton
    Context provideAppContext() {
        return appContext;
    }
}

injectする対象をメソッドの引数とするインタフェースを作成します。
Componentアノテーションを指定し、modulesにインジェクト元のモジュールを指定します。

import javax.inject.Singleton;

import dagger.Component;

@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {

    void inject(MainActivity mainActivity);

    void inject(MainFragment mainFragment);

}

Daggerのモジュールを生成します。
Daggerモジュール名でモジュールのビルダーが自動生成されます。

public class MainApplication extends MultiDexApplication {

    private static AppComponent component;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        
        component = DaggerAppComponent.builder()
                .appModule(new AppModule(getApplicationContext()))
                .build();
    }

    public static AppComponent getAppComponent() {
        return component;
    }
}

BaseActivityを生成します。
onCreateメソッドにてinjectメソッドを呼び出します。
継承するActivityクラス側でinjectする処理をします(後述)


public abstract class BaseActivity extends AppCompatActivity {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        inject(MainApplication.getAppComponent());
    }

    protected abstract void inject(AppComponent component);

}

MainActivity側ではBaseActivityを継承し
injectメソッドを実装します。
AppComponentモジュールにクラスのインスタンスを渡すことで
AppComponentのProvideアノテーションから返却される
appContextがInjectされ、MainActivity側でContextを利用できるようになります。
つまり、AppComponentを介してAppModule側でインスタンスを返却することで
呼び出し側でnewすることなく、共通変数として使うことができるようになります。

public class MainActivity extends BaseActivity {

    @Inject
    Context appContext;

    @Override
    protected void inject(AppComponent component) {
        component.inject(this);
    }


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.container, new MainFragment(),MainFragment.class.getName())
                .commit();

    }


}

Fragmentに関しても同様にBaseFragmentを作成します。

public abstract class BaseFragment extends Fragment {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        inject(MainApplication.getAppComponent());
    }

    protected abstract void inject(AppComponent component);
}

MainFragmentはBaseFragmentを継承し、AppComponentのinjectメソッドにて
MainFragmentインスタンスを渡し、外部モジュールのデータをInjectします。

public class MainFragment extends BaseFragment {

    @Inject
    Context appContext;

    @Override
    protected void inject(AppComponent component) {
        component.inject(this);
    }
}

ViewのDI

Android標準のDataBinding機能を使うと自動生成されるBindingオブジェクトでレイアウトファイルのViewの取得やモデルデータのDIができます。
イベント処理もレイアウトファイルから呼び出しの紐づけができます。
参考:Butter Knife、今までありがとう。 Data Binding、これからよろしく。

build.gradleのdataBindingをtrueに設定する。

android
{
    dataBinding {
        enabled = true
    }
}

従来の書き方(Activity)

Activityのレイアウトファイル(activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="ボタン"
            />
</RelativeLayout>

MainActivity.javaはこんな感じ

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button b = (Button)findViewById(R.id.button);
        b.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });

    }

DataBindingを使った書き方(Activity)

Activityレイアウトファイル内で
レイアウト全体をlayoutタグでくくります。
ソースファイル側からモデルなどのデータやオブジェクトを
dataタグでバインディング(参照)することができます。

<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>
        <variable name="handlers"
            type="com.example.daiki.androidtemplate.BaseHandlers"/>
    </data>


    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="ボタン"
            android:onClick="@{handlers::onClick}"
            />

    </RelativeLayout>
</layout>

Activity側はDataBindingUtilクラスを使うことでレイアウトファイルをデータバインディングすることができます。

class MainActivity extends AppCompatActivity {
  private ActivityMainBinding binding;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    binding.button.setText("ボタン"); // bindingオブジェクトにレイアウトのViewが保持されている
    // イベントオブジェクトをDI
    binding.setHandlers(new Handlers());
  }

  public interface BaseHandlers {
      void onClick(View view);
  }

  // Event
  public class Handlers implements BaseHandlers {
      public void onClick(View view) {

        if(view.getId() == R.id.button){
        }
      }
  }
}

OnCreateメソッドでの煩雑なviewやリソース取得処理、イベント処理がなくなりすっきりしました。

従来の書き方(Fragment)

Fragmentの場合、従来は次のようにonCreateViewしていました。

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fancy_fragment, container, false);
    return view;
  }

DataBindingを使った書き方(Fragment)

DataBindingの場合、
DataBindingUtilオブジェクトのinflateメソッドを呼び出し、getRootメソッドを返します。

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        binding = DataBindingUtil.inflate(inflater,R.layout.fragment_main, container, false);
       
        return binding.getRoot();
    }

DataBindingとDagger2を組み合わせる場合

DataBindingと前述のDagger2は相性が悪いです。
DataBindingをしているActivityやFragment内の変数にDagger2で直接injectすると次のようなコンパイル警告がでます。

Generating a MembersInjector for **Fragment. Prefer to run the dagger processor over that class instead.

これはコンパイル順番により、
Daggerのコンパイラが自動生成するソースコードとAndroid DataBindingが自動生成するソースコードの整合性が取れなくなるためです。
ActivityやFragmentに直接injectするのではなく、インナークラスや別クラスにinjectさせることで回避することができます。
ビジネスロジックとUIも分離できるため、こちらの方が良いかもしれません。
次のようにFragmentを修正します。

public class MainFragment extends BaseFragment {

    public class Component{
       @Inject
       Context appContext;
    }
    private Component component;
    
    @Override
    protected void inject(AppComponent appComponent) {
        component = new Component();
        appComponent.inject(component);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        binding = DataBindingUtil.inflate(inflater,R.layout.fragment_main, container, false);

        return binding.getRoot();
    }
}

AppComponentを次のように修正します。

import javax.inject.Singleton;

import dagger.Component;

@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {

    void inject(MainActivity.Component component);

    void inject(MainFragment.Component component);

}

オンライン画像取得

キャッシュもしてくれるし、利用が簡単なPiccassoが良いです
build.gradleにてライブラリダウンロード

dependencies{
    compile 'com.squareup.picasso:picasso:2.5.2'
}

ネット上から読み込みは非同期で取得してくれます。
キャッシュクリアも簡単

ImageView imageView = (ImageView)findViewById(R.id.imageView);
String url = "http://i.imgur.com/DvpvklR.png";
// 画像読み込み
Picasso.with(MainApplication.getContext())
       .load(url)
       .into(imageView);
// キャッシュクリア
Picasso.with(MainApplication.getContext()).invalidate(url);

ネットワークから画像取得する場合は
AndroidManifest.xmlに通信許可が必要です。

<uses-permission android:name="android.permission.INTERNET"/>

横断的データの反映

ActivityやFragmentをまたがって参照されるデータを監視し、更新時に反映したい場合があります。この場合、EventBusというライブラリが便利です。
EventBusはPub/Subの仕組みでデータの変更をSubscriberに一括送信します。
EventBus-Publish-Subscribe.png

build.gradleに追加します。

dependencies {
      compile 'org.greenrobot:eventbus:3.0.0'
}

Publisherを作成します。
EventBus.getDefault().postでPublishします。

public class Publisher{
    public Publisher() {
    }

    public void post(final String msg) {

        new Handler().postDelayed(() -> {
            EventBus.getDefault().post(new MessageEvent(msg));
        }, 3000);
    }

}

MessageEventクラスはメッセージを保持する単純なクラスです。

public class MessageEvent {
    public String msg;

    public MessageEvent(String msg){
        this.msg = msg;
    }
}

受信側は
EventBus.getDefault().registerメソッドで自身を登録し
Subscribeアノテーションをつけたメソッドでイベントを待ち受けできるようになります。
EventBus.getDefault().unregisterメソッドで登録解除を忘れないようにします。

@Override
public void onStart() {
        super.onStart();
        EventBus.getDefault().register(this);
}

@Override
public void onStop() {
        EventBus.getDefault().unregister(this);
        super.onStop();
}

public class MainFragment extends Fragment {
    @Subscribe
    public void onEvent(MessageEvent event){
    }

呼び出しは次のように行います。

new Publisher().post("テスト");

データ取得系の通信処理等でデータ取得時に一斉にActivityやFragmentに反映するときに便利です。

マテリアルデザイン

デザイン思想だけ投げっぱなしで実装に非常に困る箇所
マテリアルデザイン自体の仕様も完全fixしてるわけではない
Googleの公式サポートライブラリも対応しきれていないし
足りない部分はサードパーティライブラリを利用して補うしかない
FloatingButtonはAndroid4に対応しきれていないし(一部属性値が対応していない)
BottomNavigationもスクロール対応していないため外部ライブラリ使ったほうが良いです。
これに関しては中途半端なもの出してんじゃねぇとGoogleに物申したい
Material Design?言いたい事はわかった。だがどうやって実装しろと?そんなあなたに贈るMaterial Design対応ライブラリ集

  • Dialog
  • BottomNavigation
  • Font
dependencies{
    // Google公式
    compile 'com.android.support:design:25.0.0'
    // Dialog
    compile 'com.afollestad.material-dialogs:core:0.9.1.0'
    // BottomNavigation
    compile 'com.roughike:bottom-bar:2.0.2'
    // Font
    compile 'uk.co.chrisjenx:calligraphy:2.2.0'
}  

Calligraphyに関してはこの記事が参考になります。
AndroidでNotoフォント・Robotoフォントを使う

クラッシュログ

AndroidのOSバージョンや
デバイスハード周りの機能(動画再生、Bluetooth、カメラ)などは機種依存する
全て検証することは実質不可能
事前に対応できれば良いが、後で対応する事態はどうしてもでてくる。

デバイス周りの問題は
FabricのCrashlyticsでクラッシュログを取得できるようにする

インストール方法は下記を参考にしてください
[Android][Fabric] Crashlytics を使って Android アプリのベータ版を配信する

クラッシュはしなくとも機能的に起こってほしくない場合の箇所には
カスタムログを送信できるようにする

参考:CrashLytics カスタムレポート拡張 Android

メモリリーク対策

メモリリーク検出にはLeakCanaryが使える
ActivityやFragmentライフサイクルの終了時には
通信キャンセルおよび不要変数にはnullを代入してGC(ガベージコレクション)の動作を促す

dependencies{
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
    testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
}

ApplicationクラスのonCreateメソッドにLeakCanaryの初期化処理を追加

public class MainApplication extends MultiDexApplication {

    private static AppComponent component;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }
        LeakCanary.install(this);
    }
}

LeakCanaryアプリでメモリリークが検知できます。
詳細は次記事のほうがわかりやすいです。
LeakCanaryでメモリリークを検出する

テストアプリ配信

apkがあればぶっちゃけGMail経由でもインストールできるのですが
DeployGateにapkをアップロードしてそこ経由でダウンロードしたほうがURLを教えるだけでインストールできるので楽です

プラグイン

入れとくといろいろはかどります。

Dagger IntelliJ Plugin

164
191
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
164
191

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?