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内に書くことでデバッグ時、リリース時に固定値を切り替えることができます。
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コード側での固定値の取得は次のようになります。
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:コントロールの基本色
AndroidManifest.xmlのapplicationタグにテーマが設定されている
<application
android:theme="@style/AppTheme">
ライフサイクル
Activityのライフサイクル
Fragmentのライフサイクル
画面設計
AndroidではFragment移動時にはBackStackという領域にFragmentを保存する。
遷移元から2方向以上で循環できるような構造があると厄介なことになる。
TabとかBottom Navigationとかだと一つのActivity内で複数のFragmentを保持するため、この問題が起こりやすい。(タブ内のFragmentの2階層以上深い階層にTabやBottom Navigationが存在する場合)
上記の場合、BackStackをタブ別に持つなどの実装になるため、実装は非常に複雑。
そもそもAndroidの画面遷移構造に適していない。
ちなみにBackボタン(BackStack)とUpボタンの挙動は次のようなガイドラインに沿うのが推奨されています。
Back ボタンと Up ボタンを使用したナビゲーション
設計・ライブラリ
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に記述します。
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を導入するには次のようにします。
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で戻り値を定義していることに注意してください
interface UserApiClient {
// ランダムユーザ取得API (https://randomuser.me/api)
@GET("api")
Observable<JsonObject> getUser();
}
OkHttp3、Gson、Retrofit2のインスタンスを生成し、
API呼び出し&データ取得部分に関しては次のようになります。
Retrofit2生成までは生成処理が重いため、一度生成したら使いまわした方が無難です。
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での変更点
上記の例だと次のように書き直せます
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にてインストール
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に一括送信します。
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を教えるだけでインストールできるので楽です
プラグイン
入れとくといろいろはかどります。