Architecture Componentを触ってみた

とりあえず軽く触ってみた。

codelab

Google IOの動画はとりあえず2つ見てみた。

最初のやつは「こんなん作ったでー」という話で、2つ目がその中身を説明っていう感じなので、時間がないなら2つ目だけ見ればいいんじゃないかなと思う。全部英語なので、私の英語力では雰囲気しかわからず時間対効果はあまり良くなかった気がしている。

3つ目もあるのだけど、これはまだ見れていない。タイトルから見るとRoomについての説明なのだろうか。

個人的にArchitecture Componentで気になっていたのは、いかにActivityのライフサイクルに振り回されなくてすむようにできるかという部分だ。つまり、LifecycleOwnerLiveDataViewModelについてである。とりあえずその観点で言うと、最初にあげたcodelabを触れば、雰囲気はわかった。2つ目の動画でだいたいスタンスがわかったような気がしている。

たぶん2つ目の動画で話していたと思うのだけど、このArchitecture Componentはすでに個々の開発者がそれぞれの工夫でActivityなどのライフサイクルによる呪縛を回避している手段を置き換えるためのものではないという話が、個人的にはしっくり来た。すでにライフサイクルとの付き合い方がうまくできている人は、別にそれでいいと。

ただ、Androidをこれから学ぼうとする人にとっては、Android特有のライフサイクルにまつわるあれこれは、学習していく上でつまづきやすいポイントで、さらにそれを回避するためのライブラリの使い方を学ぼうとすると余計にややこしくなってしまう。そこで、これからAndroidを学んでいく人にとって、とっつきやすいシンプルな仕組みを用意したよ、というのがArchitecture Componentということらしい。

私は最近ではDaggerやRxJavaを使って、Activityにはデータを持たせない、単に表示するだけのものとして扱うようにしてアプリを作るようにしている。そういった方法ですでにうまいこと回せているなら、無理して移行する必要はないのだろう。すでにうまいことやっている人にとっては、ちょっと触れば雰囲気がつかめるだろうから、とりあえずcodelabだけ触って雰囲気を掴んでおいて、1.0が出るのを待つくらいのスタンスでいいんじゃないかなと思う。

以下は触ってみた感想。

LifecycleOwnerとは

ざっくりした理解で言うと、ActivityとかFragmentとかServiceとか、Android特有のライフサイクルをもってるオブジェクトのことという認識でいる。

LiveDataの購読を行う際に引数に指定してやったり、ViewModelの生成・取得を行う際に引数に渡したりするのに出てくる。

LiveDataの購読に関しては、LifecycleOwner(Activityとか)のライフサイクルにあわせて自動的に購読解除してくれるらしい。つまり、いちいち自分でunsubscribe/disposeとかしたりしなくていいっていうこと。

またActivity等のライフサイクルにあわせた処理を行うのに利用したりできるっぽい。(codelabではLocationManagerから位置情報を受け取るクラスを作って、Activityのライフサイクルにあわせてセンサーの登録と解除を行うのに使っていた)

LiveData

UIに表示したりする実際のデータ。DataBindingでいうObservable<Hoge>みたいなものだし、RxJavaでいうObservable<Hoge>みたいなもの。

その実態は最新の値を保持してよしなに通知してくれるデータホルダー。RxJavaみたいなストリームではないよということだ。またスレッドの概念も持ってないので、バックグラウンドで処理してメインスレッドで通知みたいなことはしない。

LiveDataの更新はメインスレッドでないとできないみたいで、別スレッドからLiveData.setValue()したら落ちた。別スレッドから値を更新したい場合は、postValue()を使うらしい。

ActivityなどのViewは、このLiveDataを購読して、変更を受け取ったらUIを更新することだけ考えるような作りにするのが良いのだろう。

RxJavaでいうBehaviorSubjectみたいな動きをするなぁという印象を持った。

ViewModel

画面回転してもライフサイクルが継続してくれるもの。これが最初からあればAndroidアプリ開発はもっと楽になっていただろうと思う。

今までActivityにもたせていた状態やロジックを、全部こっちに持ってくればうまいことできると思う。

Activity.finish()を呼び出したらViewModelのライフサイクルも終了する(ViewModel::onCleard()が呼び出される)。

画面を回転させた場合はそのまま以前の状態を引き継いだものが、再生成されたActivityに渡ってくる。ただ、あくまでonConfigurationChangeで破棄されないだけなので、ホーム画面に一度移動した後にOSによって終了されたりした場合(Activityを保持しないが有効になってたりした場合)はViewModelのライフサイクルも終了してしまう。

データの永続化は別途考えないといけない。

軽く触ってみて

RxJavaなどの知識を持っているからか、割りとすんなり使えそうな気がしている。

それを抜きにしてもそんなにややこしくないと思うので、Androidをこれから学ぼうという人はとりあえずArchitecture Componentの使い方を学ぶと、シュッと入門できていいんじゃないだろうか。

触る前は「なんかいろいろあってややこしそうだな」とか思っていたのだが、触ってみると思いの外それぞれ独立していて、使いたいコンポーネントだけ利用すればいいという意味がよくわかった。

私個人としては、ViewModelはすぐにでも使いたい。Daggerを使って似たようなことをやっていたけれども、ViewModelを使うほうが楽だ。LiveDataはもうちょっと調べて、DataBindingとの組み合わせ方を掴んだら置き換えるかもしれない。

ActivityScopeを使ってActivityクラスごとにシングルトンになるようにした話

まずはじめに。Dagger2の話をしていますが、きちんと理解しているわけではないので間違った内容があるかもしれません。鵜呑みにしないでください。

この記事で言ってることのサンプルコードはGutHubで公開しています。

この記事の要旨は「MainActivityの中でシングルトンを実現したい(した)」ということです。

Dagger2を知ったキッカケ

私がDagger2を知ったきっかけはdroidkaigi2016です。その時からずっと腑に落ちなかったのがActivityScopeの存在です。ActivityScopeがわからないというか、子コンポーネントをわざわざ作る意義がわからなかったのです。

droidkaigi2016のコードを見よう見まねでDagger2を使ったアプリを作ったのですが、そのアプリではほぼすべての依存性をAppModuleに定義してありました。そのためAppModuleだけがやたらと肥大化し、ActivityModuleには何も定義されていないような状態で、ActivityScopeを作っている意味がまったくありませんでした。

結果そのアプリでは、Dagger2を依存性の充足のために用いるのではなく、シングルトンパターンを使うことなくアプリ内でインスタンスが1つになるようにするための道具として使っている状態でした。

ActivityScopeでやりたかったこと

私は、例えば端末の画面が回転しても、同じMainActivityであれば常に同じコントローラなりPresenterなりがセットされるようにしたいと思っていました。そうすれば非同期処理を引き継ぐためにアレコレする煩雑さから解放されます。

それを実現するために子コンポーネントを区切ってActivityScopeを作ってるんだろうと考えていたのですが、実際の挙動はそうはなりません。ActivityModuleで@ActivityScopeなんて指定したところで、画面回転したら注入されるのは異なるインスタンスです。

(これはActivityComponentをActivityのonCreateで初期化して、Activityがそのインスタンスを保持していることに原因がありましたが、詳細は後述)

そもそもActivityのライフサイクルは非常に短命で、初心者がまず躓くポイントとして挙げられるほどに感覚値とずれたものです。画面回転しただけでインスタンスが変わる。同じMainActivityなのに。同じMainActivityが表示されてるのに、実は内部では異なるインスタンスのものなんですというのがややこしいポイントです。

私はずっとActivityScopeを使えば、同じMainActivityなら常に同じインスタンスを注入できるようになるんじゃないかなと思っていました。でもそれができない。それが私の「Dagger2よく分からん」の原因の1つでした。

Dagger2がインスタンスをシングルトンのように扱うことが出来る仕組み

そもそもDagger2でインスタンスを使いまわせるのは、スコープをアノテーションで指定しているからではありません。ApplicationModuleで@Singletonを指定したインスタンスが常に同一であるのは、アプリ内で同じApplicationComponentを参照しているからできていることです。

ApplicationComponentはApplicationクラスを拡張した独自のクラスに保持して、そこにアクセスしていると思います。例えばこれを、(普通そんなことはしませんが)ActivityのonCreateでApplicationComponentを生成するようにしたらどうなるでしょう。画面回転によるActivityの再生成が起こる度に@Singletonとしたインスタンスであろうが毎回異なるインスタンスが注入されることになります。つまり@Singletonをつけてるからシングルトンになるわけではないということです。

ActivityのonCreateでApplicationComponentを生成した場合、同じActivityのインスタンス内ではシングルトンにできます。例えばそのActivityでViewPagerを使っていて、Fragmentを複数内包しているとしましょう。そのFragmentたちはActivityのもつApplicationComponent(ややこしい)にアクセスすることで、@Singletonで指定したインスタンスを使いまわすことが出来ます。

ApplicationクラスでActivityComponentのインスタンスを保持

同じActivityクラスでActivityComponentを使いまわせるようにするためには、Activityより長いライフサイクルを持つものにComponentのインスタンスを保持してもらう他ありません。

私はとりあえずApplicationクラスにActivityクラス名をキーとしたHashMapを持たせて管理させるようにしました。CustomApplication.classのソースコード

こうすることで、例えばFilterEditActivity.classでは、画面回転でActivityのインスタンスが変わろうと、常に同じActivityComponentを取得でき、FilterEditAcitivy内で常に同じPresenterが使えるようになります。

デメリット

ほんとうの意味でのActivityのライフサイクルと異なるわけなので、逆にわかりづらくなっている気がしないでもありません。ActivityScopeといいながらその生存期間はApplicationと同じになってしまっています。

Fragmentを使う場合に更に混乱します。実際にサンプルのプロジェクトでは、Activity+ViewPager+Fragmentを使う部分でややこしいことになっています。

Activityの場合はActivityクラスで識別すればいいのですが、ViewPager内のFragmentはクラス名で識別することが出来ません(同じクラスでも中身が異なるため)。

またActivityContextを注入したい場合に困ります。まず間違っても@ActivityScopeで定義してはいけません。それをやると一番最初に生成されたActivityのインスタンスが使いまわされることになってしまいます。ただスコープをつけなくても、Componentが参照しているActivityContextは最初に作成されたActivityのインスタンスとなってしまうので、スコープをつけないだけでも足りません。

このサンプルプロジェクトでは、苦肉の策としてApplicationクラスにActivityModuleのインスタンスも一緒に管理させるようにしています。ActivityModuleがもつContext(Activity)を更新するためです。

しかしそうやったところで、ActivityScopeで使いまわしたい何らかのインスタンスに、ActivityContextを持たせなければならない場合はどうしようもありません。サンプルプロジェクトでは幸いActivityContextに依存するものがないのでなんとかなっていますが、将来的には不明です。

変な依存を産んでしまっている気がしないでもない

依存性をなくすためのDagger2で、逆に変な依存を産んでしまっているような気がしないでもありません。

Read full post gblog_arrow_right

Realmのテストのやり方を知りたい

Realmを使ってみました。ちなみに私は、今まではGreenDAOとAndroid Ormaしか使ったことがありません。

とりあえずCRUD操作のやり方をつかもうとテストを書いてみました。テストの書き方が根本的に間違っている可能性が無きにしもあらずですが、こんな感じで作りました。

public class FilterDataSourceRealmTest {
    private static RealmConfiguration    config;
    private static FilterDataSourceRealm sut;

    @BeforeClass
    public static void initializeTest() {
        config = new RealmConfiguration.Builder()
                .name("test_realm")
                .deleteRealmIfMigrationNeeded()
                .build();
        sut = new FilterDataSourceRealm(config);
    }

    @Before
    public void setUp() {
        Realm.deleteRealm(config);
    }

    @After
    public void tearDown() {
        Realm.deleteRealm(config);
    }

    @Test
    public void insertFilter() throws Exception {
        final CountDownLatch latch = new CountDownLatch(1);
        sut.insertFilter("test.com/");
        sut.getFilter("test.com/")
           .subscribe(new Action1<UriFilter>() {
               @Override
               public void call(UriFilter uriFilter) {
                   assertThat(uriFilter.getFilter(), is("test.com/"));
                   latch.countDown();
               }
           });
        latch.await(2, TimeUnit.SECONDS);
    }
}

テスト対象のコード(一部抜粋)はこんな感じです。

public class FilterDataSourceRealm implements FilterDataSource {
    private RealmConfiguration config;

    public FilterDataSourceRealm(RealmConfiguration config) {
        this.config = config;
    }

    @Override
    public void insertFilter(String insert) {
        Realm realm = Realm.getInstance(config);
        realm.beginTransaction();
        realm.copyToRealmOrUpdate(new UriFilter(insert));
        realm.commitTransaction();
        realm.close();
    }

このテストコードはandroidTestに配置して実機で実行します(Instrumentation Test)。

Read full post gblog_arrow_right

DataBindingを使っていてexecutePendingBindingsを呼び出さないとどうなるか

私はfindViewByIdをしなくていいからという理由でDataBindingを使っています。利用するためにbuild.gradleに

dataBinding {
    enabled = true
}

とするだけでいいのも気に入っています。

今回RecyclerViewのViewHolderにDataBindingを適用したときに、executePendingBindings()を呼び出さないことによる弊害がわかったのでご紹介します。

https://developer.android.com/topic/libraries/data-binding/index.html#advanced_binding

ドキュメントにはholder.getBinding().executePendingBindings();と、executePendingBindings()を呼び出すように書いてあります。私はDataBindingを使っていて、このようなメソッドを呼び出したことがなかったので、「なんでいるんだろう?」と疑問に思いました。オブジェクトのバインドはスケジュールされるだけですぐに行われるわけではないと書いてますけど、これまで使わずとも特に問題を感じなかったから、別になくてもいいのではと思ったのです。

私はこんな感じで使ってました。(Adapterのコードの一部ですが)

    @Override
    public void onBindViewHolder(DataBindingViewHolder<ItemHatebuFeedBinding> holder,
                                 int position) {
        ItemHatebuFeedBinding binding = holder.getBinding();
        final HatebuFeedItem  item    = items.get(position);
        binding.setItem(item);
    }

レイアウトファイルはこんな感じです。

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    >

    <data>
        <variable
            name="item"
            type="jp.gcreate.sample.daggersandbox.model.HatebuFeedItem"
            />
    </data>

    <LinearLayout
        style="@style/RecyclerViewContainer.Clickable"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingBottom="@dimen/item_padding_with_item"
            >

            <TextView
                android:id="@+id/count"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="@dimen/item_padding_with_item"
                android:text="@{String.valueOf(item.count)}"
                android:textAppearance="@style/TextAppearance.AppCompat.Title"
                android:textColor="@color/red_600"
                />

            <TextView
                android:id="@+id/title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@{item.title}"
                android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
                android:layout_gravity="fill_horizontal"
                />


        </LinearLayout>

        <TextView
            android:id="@+id/description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{item.description}"
            android:paddingBottom="@dimen/item_padding_with_item"
            />

        <TextView
            android:id="@+id/date"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{item.date}"
            android:textAppearance="@style/TextAppearance.AppCompat.Caption"
            />

    </LinearLayout>
</layout>

executePendingBindings()を呼び出さなくても普通に動作します。下に向かってスクロールする分には何も変なことはありません。しかし、下から上に向かってスクロールすると、時折妙な動き方をします。時折ブレるような挙動をするのです。(ちなみに動画を撮って用意したのですが、ファイルサイズが大きいので貼るのは止めました)

この動きはexecutePendingBindings()を呼び出していると起こりません。なるほど、executePendingBindings()を呼び出さないとこのようなことになるわけですね。

微妙にブレるように感じたのは、RecyclerViewをスクロールして次のViewが要求される→onBindViewHolderが呼び出され、setItem()でオブジェクトをバインドする→Viewが見え始める→バインドしたオブジェクトが実際にViewに描画される→中身によってViewの高さが変わる→アイテムが見え始めてからViewの高さが変わり、表示中のアイテムが動いたようにみえる、という経過を辿っているのでしょう。

下に向かっていく分には、Viewの高さが変わっても伸びた部分は画面外にいくので、特に違和感を感じません。しかし、上に戻っていくときにはViewが見え始めてから高さが変わるため、下に伸びるとそれまで表示していた部分が下に押し出されて、自分がスクロールした分以上にスクロールしたように感じる。もしくは短くなった場合には、スクロールしたのが取り消されて上に引っ張られたかのように感じる。それが違和感の原因でした。

これは各アイテムのViewの高さが一定であれば生じない問題です(高さのズレが生じなくなるため)。この例ではwarp_contentを使っていて、かつ中身の長さがアイテムによって異なっていたために生じました。

これまで特にDataBindingによるタイミングのズレなど気にしたことがなかったのですが、RecyclerViewで使うときには気をつけないといけないんですねぇ。

Daggerを使ってSingletonにする仕組み

ものすごいあほうなことを書いているかもしれませんが、そのときはご指摘ください。

Daggerを使って依存性を注入する際に、アプリ内でSingletonになるようにすることあるじゃないですか。

@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {
    void inject(MainActivity activity);
}

@Module
public class AppModule {
    private Context context;
    public AppModule(Context context) {
        this.context = context;
    }

    @Provides
    @Singleton
    public SomeClass provideSomeClass() {
        return new SomeClass().initializeWithDefault();
    }
}

みたいに、SomeClassがアプリ内でシングルトンになるようにすると。

今までずっと、@Singletonって指定してるから実現できてるんだと思っておりました。実際には違います。これはそもそもAppComponent自体がアプリ内でシングルトンになっていなければ実現されません。

このAppComponentはApplicationクラスを拡張して、そこで初期化してるから@Singletonという指定が効くのです。このAppComponentを、ActivityのonCreateで初期化していたらシングルトンにはなりません。AppComponentインスタンスの中ではSomeClassのインスタンスは一度生成されたら使いまわされますが、AppComponentのインスタンスが複数生まれてしまえば生成されるSomeClassもAppComponentのインスタンスの数と同じだけ増えていくことになります。

そして@Singletonは別に@Singletonでなくてもいいのです。自分でスコープを作って、例えば

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

@Module
public class AppModule {
    @Provides
    @AppScope
    public SomeClass provideSomeClass() {
    }
}

としても結果は同じです。Componentにつけたスコープ名の中でインスタンスを使いまわすっていう感じになるわけです。

だから@Singletonつけてるからシングルトンになるわけではないのです。AppComponentのインスタンスがアプリ内で1つだからこそ、シングルトンにできているわけです。

ここがあやふやなままだったので、Daggerよく分からん状態だったのですが、これで一歩前進できます。

Crashlyticsのクラッシュレポートをdebugビルドでは行わないようにする

新しいアプリを作るときにしかやらないので、いつもやり方を忘れてしまうCrashlyticsのレポート設定のメモ。

デバッグビルドでまでクラッシュレポートが報告されてしまうと、通知がうっとうしいだけですし、手元でスタックトレースが読めるわけですから完全に無駄です。必要なのは基本的にはリリースビルドだけでしょうから、開発中は無効にしてしまうのが便利です。

やることは下記のページのとおりです。

https://docs.fabric.io/android/crashlytics/build-tools.html?gradle#build-tools

まずはレポートを無効にしたいbuildTypeにext.enableCrashlytics = falseを追加します。

この設定はGradleのビルド時に、Crashlyticsで使うIDなんかを生成する処理を行わないようにするもののようです。レポート送信を止めるわけではないので、これを追加しただけでは下記のようなエラーが出てしまいます。

This app relies on Crashlytics. Please sign up for access at https://fabric.io/sign_up,
                                                   install an Android build tool and ask a team member to invite you to this app's organization.

開発時にレポート送信自体を止めるためには、アプリ実行時にCrashlytics自体が動かないようにする必要があります。そのためには、Crashlyticsの初期化を行う部分で、下記のように初期化を行います。

        Crashlytics crashlyticsKit = new Crashlytics.Builder()
                .core(new CrashlyticsCore.Builder()
                        .disabled(BuildConfig.DEBUG).build())
                .build();
        Fabric.with(this, crashlyticsKit);

ちなみにdebuggableがfalseなんだけどCrashlyticsのレポートを無効化したい、なんていう場合には、.disabled(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("test"))のようにしてやればOK。

レポートが送信されないことの確認

アプリの任意の場所でthrow new RuntimeException("Crashlytics report test");と例外を投げてみればOK。

ただし例外を投げる場所に注意が必要です。Crashlyticsの初期化が終わっていないと、そもそもレポートが送信できません。Applicationクラスの外か、ActivityのonCreateの外で例外を投げるようにしないとテストにならないので注意しましょう。

https://docs.fabric.io/android/crashlytics/introduction.html

DataBindingを試す

DataBindingがアツいらしいと聞いて試してみました。簡単な使い方をするなら想像以上に簡単でした。

今までActivityなどでfindViewByIdを書きたくないから、ButterKnifeをどのプロジェクトでも使っていたのですが、DataBindingを使えば同じようなことができます。

両者を使ってみて感じたのは、ButterKnifeがレイアウトXMLをJavaコードに持ってくるイメージであるとすれば、DataBindingはJavaコードをレイアウトXMLに持っていくイメージであるということです。

DataBindingを使うことで、Javaで作成したコードを、レイアウトXMLに埋め込むことができるようになります。レイアウトXMLでどのデータを使うか指定しておけば、Activityで「このクラス(のインスタンス)を使ってくれ」と指定するだけでその内容を表示できたりします。

具体的な使い方はData Binding Guide – Android Developersを参照してください。

DataBindingを使う設定

Android Studio 1.3以上であることが必須です。

Android Gradle Plugin 1.5.0-alpha1以上を使っていることが必須、でした。

Android Studio 2.0 betaになると、コード補完のサポートがより強力になってます。

build.gradleでDataBindingの設定を有効にすることで利用できます。

android {
    ....
    dataBinding {
        enabled = true
    }
}

表示するデータを保持するクラスの作成

public class Character{
    public String name;
    public int age;
    public String skill;

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

}

DataBindingを使ってアクセスするには、publicなフィールドであるか、privateなフィールドである場合publicなgetterがあることが必須です。

レイアウトXML

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    >

    <data>
        <variable
            name="chara"
            type="jp.gcreate.sample.databinding.Character"
            />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="jp.gcreate.sample.databinding.MainActivity"
        >

        <TextView
            android:id="@+id/chara_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{chara.name}"
            />

        <TextView
            android:id="@+id/chara_age"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{Integer.toString(chara.age)}"
            />

        <TextView
            android:id="@+id/chara_skill"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{chara.skill}"
            />

    </LinearLayout>

</layout>

ポイントはこんな感じ。

Read full post gblog_arrow_right

jarファイルで配布されているライブラリをAndroid Studioで取り込む

Android StudioはビルドツールにGradleを使っているので、ライブラリはbuild.gradleのdependenciesに書くことで簡単に取り込むことが出来ます。

しかし、ライブラリによってはjarファイルで配布されているものもあります。(この例ではNiftyのMobile backend)

jarで配布されるライブラリを組み込む手順は簡単です。app/libsディレクトリにjarファイルを置くだけで完了です。

これはapp/build.gradleにて、libsディレクトリにあるjarファイルをビルド時にコンパイルするよう指定されているからです(compile fileTreeの部分)。

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.+'
    compile 'com.android.support:design:23.+'
}

libsディレクトリなんかない、という場合、プロジェクトビューがAndroidになっている可能性が考えられます(デフォルトではAndroidになっています)。この場合、Projectに表示を切り替えることでディレクトリ階層が表示されるようになるはずです。

それでも見つからなければappディレクトリの下にlibsディレクトリを作成し、app/build.gradleにcompile fileTree(dir: 'libs', include: ['*.jar'])を追加すれば組み込めると思います。

ライブラリの取り込み

support libraryのバージョンの調べ方

app/build.gradleのdependenciesにさり気なく書いてあるサポートライブラリ。

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.1'
}

appcompat-v7:22.2.1のこの22.2.1の部分。自分で指定しようと思うと、バージョンいくつが存在しているのかが分からず、どうやって確認すればいいのかも分かりません。とりあえず最新のものが当たればいいやと、+を使ってごまかしたりしてきましたが、最新のものが当たるとそれはそれでうまくいかないことがあったりします(23が出てるんだけど、とりあえずはtargetSDKを22で作ってる今とか)。

要するに、1つ前のバージョンを指定したいのだけど、その1つ前のバージョンというのはいったいいくつなんだというのが困ります。結論から言うと22.2.1なんですけど、じゃあそれをどうやって調べたらいいのかという話です。

Support Library – Android Developersで確認できます。

また、パソコンにインストールしているAndroid SDKのディレクトリを潜って行くことでも調べることはできます。

(Android SDKのインストールディレクトリ)/extras/android/m2repository/com/android/support/appcompat-v7

このディレクトリに、バージョンごとのフォルダがあるのでそこでも確認可能です。

そもそも指定できるバージョンの候補を出してくれると楽なんですけどね。

UndoBarを使った削除処理を考える

UndoBar – GitHub

削除処理を行って、それを取り消し可能なようにする実装を考えます。例えばGmailのアプリでメッセージをアーカイブしたときなどに表示されるあれです。

削除する際に「本当に消しますか?」みたいな確認ダイアログを表示するのはナンセンスっぽいです。

なぜならその確認ダイアログは、データが消えてアクセスできなくなるという責任をユーザーに転嫁してるだけだということです。そもそも削除したいだけなのにいちいち確認されるのもうっとおしいだけではないかという理由もあります。

それならば、もし消したくないデータを誤って削除してしまったとしても、それを復元できるようにするのがユーザー目線でいいだろって話です。

この辺りの話はSmashing Android UI レスポンシブUIとデザインパターンという本で知りました。

では実際に取消可能な削除機能を実装するためにはどうすればいいかというと、UndoBarを利用するのが簡単そうな気がします。Crouton使おうかと思ったけども、いざ使おうと思ったらレイアウトを実装したりするのが~~面倒~~大変だったので、シンプルなUndoBarを使いました。ソースコードはCroutonの方が読みやすかったけど。

UndoBarの表示

今回私がやりたかったのは、データベースに保存してあるデータを消すという例です。データベースに保存されているデータをListViewで表示している。そのListViewのうちの1つのアイテムを選んで削除をするというパターン。

実装方法としては削除の操作を行った時(UndoBarを表示させる時点)でどうするかによって、2パターンに分岐するかと思います。

  1. この時点で実際にDB上のデータを消してしまう
  2. この時点ではDBは触らず、見た目上のデータを消す
どちらのパターンにせよ、この時点でListViewに表示されてるデータを消すのは共通です。データベースのデータを消すタイミングがここかどうかです。

1.のパターンは見た目と実装がそのままリンクしています。Listから消えたらデータベースからも消える、シンプルです。

このパターンの場合は操作の取消を選んだら、消したデータを復元する必要があります。そのためどうやってデータをロールバックするのかに頭を悩ませることになります。データベースの構造が複雑だと元に戻すのが大変かもしれません。

2.の場合はUndoといいつつ、実際には削除処理を猶予しているだけです。このパターンの場合、取消操作はデータベースからデータを削除するのを取り消す処理(ややこしい)になります。

こちらは復元処理を考える必要がありません。一方でUndoBarが消える前にアプリを終了したり、画面が回転したときどうすんのっていう新たな問題が生じます。

UndoBarからのコールバック

UndoBarController.AdvancedUndoListenerを使うことで3つのタイミングによるコールバックを受け取ることができます。

この3つのコールバックをうまいこと利用して削除機能を実装していくことになります。

onUndo()

取消ボタンを押した際に呼ばれます。Undo時の処理をここで行うと良いです。

パターン1のときならここで復元処理を行うことになります。

onHide()

取消ボタンが押されず、UndoBarが消える際に呼ばれます。UndoBarに設定した表示時間が経過した後でコールされるわけです。UndoBar表示中に画面回転した場合には呼ばれません。

ちなみに画面回転に対応するには、自分でハンドリングしてやらないといけないそうです。今回私はそこまで踏み込みませんでした。

onClear()

UndoBar.clear()を明示的にコールした際に呼ばれます。

Read full post gblog_arrow_right