visible true

技術的なメモを書く

TextViewが省略されたかどうかを確認する

ググってもだいたいEllipsizeの設定で自動省略の話しか出てこないのでメモる。

やりたい事

TextViewが省略表示になっているかを確認して処理したい、というモノ。

isTextTruncated()を作る

TextViewUtilsとか適当に作る。おおむねgetLatyout()とかgetLineCount()とかgetEllipsisCount()とか使うと省略かどうかわかる

public class TextViewUtils {
  public static boolean isTextTruncated(TextView textView) {
    if (textView == null) {
      return false;
    }
    Layout layout = textView.getLayout();
    if (layout == null) {
      return false;
    }
    int lines = layout.getLineCount();
    if (lines < 1) {
      return false;
    }
    int ellipsisCount = layout.getEllipsisCount(lines - 1);
    return ellipsisCount > 0;
  }
}

こういう時に使える

ほぼ存在しないとおもうけどこんなケース

f:id:sys1yagi:20150929172825j:plain

View作成直後は省略されたか分からないのでonSizeChanged()のタイミングとかでやる。

public class CustomView extends LinearLayout {
  TextView text;
  public CustomView(Context context, AttributeSet attrs) {
    super(context, attrs);
    LayoutInflater.from(context).inflate(R.layout.content_main, this);
    text = (TextView) findViewById(R.id.text);
  }
  @Override
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (TextViewUtils.isTextTruncated(text)) {
      //なんかレイアウト変える
    }
  }
}

Custom Viewじゃない場合ViewTreeObserver.OnGlobalLayoutListenerとかでやればいけそうだけど大変そう。

おわりに

  • 省略されたかどうか確認できる。
  • 使いたいケースはそんなになさそう。
  • singleLine=trueの時しか使えないです

画面を開いた時にEditTextにフォーカスさせたくないのでBlockEditTextDefaultFocusLinearLayoutというViewを作った

画面を開いた時にEditTextにフォーカスさせたくない(キーボードを開かせたくない)という事で以下のアプローチを考えたがどれも上手くいかなかった。

  • EditTextを最初focusable=falseにしておく
    • focusable=trueにするタイミングが難しい
  • EditTextをfocusable=false, focusableInTouchMode=trueする
  • InputMethodManager.hideSoftInputFromWindow(IBinder, int)
    • こちらも呼び出しタイミングが難しい

あまり美しくない方法

EditTextにフォーカスを取らせない事が目的なので、他のViewを定義しそこにrequestFocusしてやればよいがこれでは意図が伝わりにくい。

<LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >
  <EditText
    android:id="@+id/message_edit"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:focusable="true"
    android:focusableInTouchMode="true">
      <requestFocus />
  </TextView>

BlockEditTextDefaultFocusLinearLayout

仕方がないのでBlockEditTextDefaultFocusLinearLayoutを作った。継承するViewは必要に応じて変えていけばよい。今回はLinearLayout。

public class BlockEditTextDefaultFocusLinearLayout extends LinearLayout {

    public BlockEditTextDefaultFocusLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setFocusable(true);
        setFocusableInTouchMode(true);
        requestFocus();
    }
}

こうなる。もっと良い命名があると思うけど大体通じるんじゃないかと思う。

<com.sys1yagi.android.views.BlockEditTextDefaultFocusLinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >

  <EditText
    android:id="@+id/message_edit"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

おわりに

もっと良い方法あったら教えてください。

※追記 2015/08/21

android:windowSoftInputMode="stateAlwaysHidden"をAndroidManifest.xmlで書くといける!!ただしこの場合requestFocus()は無視されるのでFragmentの遷移とかである時はフォーカスしたいケースなどでは使えない。そういうケースはあんまりなさそうなのでよさそう。

kvs-schemaとdagger2を同時につかう

kvs-schemaというSharedPreferenceをいい感じにクラス化出来るライブラリがあるんですが、このライブラリはJSR 269(Pluggable Annotation Processing API)を使ってコンパイル時にコード生成をしています。dagger2も同様にJSR269によってコード生成をしています。なんとなく「変な衝突の仕方しないかな?」と思ったので試してみました。案の定問題がありました。本エントリではJSR 269を使ったライブラリの衝突の問題とその回避方法について解説します。

kvs-schemaの使い方については以下のライブラリ作者が書いた解説記事を参照してください。

qiita.com

KVS Schemaを定義する

まずKVS Schemaを定義します。例として初回起動時にガイドを出すフラグをひとつだけもつSchemaを作ります。

@Table("guide")
public abstract class GuidePreferenceSchema extends PrefSchema {
  @Key("should_show_guide")
  boolean shouldShowGuide;
}

これによりGuidePreferenceが生成されます。

public final class GuidePreference extends GuidePreferenceSchema {
  public final String TABLE_NAME = "guide";
  GuidePreference(Context context) {
    init(context, TABLE_NAME);
  }
  GuidePreference(SharedPreferences prefs) {
    init(prefs);
  }
  public boolean getShouldShowGuide() {
    return getBoolean("should_show_guide", shouldShowGuide);
  }
  public void putShouldShowGuide(boolean shouldShowGuide) {
    putBoolean("should_show_guide", shouldShowGuide);
  }
  public boolean hasShouldShowGuide() {
    return has("should_show_guide");
  }
  public void removeShouldShowGuide() {
    remove("should_show_guide");
  }
}

GuidePreferenceSchemaGuidePreferenceを返却するメソッドを生やしておきます。これはGuidePreferenceコンストラクタがpackage privateなためです。

@Table("guide")
public abstract class GuidePreferenceSchema extends PrefSchema {
  @Key("should_show_guide")
  boolean shouldShowGuide;
  public static GuidePreference create(Context context) {
    return new GuidePreference(context);
  }
}

Dagger2のComponentとModuleを定義する

Dagger2のComponentを定義します。SharedPreferenceはApplicationのスコープで管理すれば良いと思うのでAppComponentおよびAppModule*1で提供する事にします。

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

GuidePreferenceはContextが必要なのでAppModuleにContextを持たせておきます。

@Module
public class AppModule {
  Context context;
  public AppModule(Context context) {
    this.context = context;
  }
  @Singleton
  @Provides
  public GuidePreference provideGuidePreference() {
    return GuidePreferenceSchema.create(context);
  }
}

あとは使う準備です。Applicationクラスを定義してAppComponentをもたせます。

public class Application extends android.app.Application {
  static AppComponent appComponent;
  public static AppComponent getAppComponent() {
    return appComponent;
  }

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

injectの対象となるMainActivityでinjectionの処理を書きます。

public class MainActivity extends AppCompatActivity {
  @Inject
  static GuidePreference guidePreference;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Application.getAppComponent().inject(this);
    //..

これで準備万端です。

コンパイルできない

ところがこの状態でコンパイルすると以下のエラーでコケます。

Execution failed for task ':app:compileDebugJavaWithJavac'.
> java.lang.IllegalArgumentException: GuidePreference cannot be represented as a Class<?>.

Dagger2がComponentやModuleを処理する時、kvs-schemaが生成したGuidePreferenceが見えないのが原因っぽいです。aptのオプション等色々見ましたが設定で回避はできなさそうでした。

生成されたクラスを直接参照しない

仕方ないのでkvs-schemaが生成するクラスを直接参照しない形にすることにしました。

public class GuidePreferenceProvider {
  GuidePreference guidePreference;
  public GuidePreferenceProvider(Context context) {
     this.guidePreference = GuidePreferenceSchema.create(context);
  }
  public GuidePreference get() {
    return guidePreference;
  }
}

AppModuleではGuidePreferenceProviderをprovidesする事になります。

@Module
public class AppModule {
  Context context;
  public AppModule(Context context) {
    this.context = context;
  }
  @Singleton
  @Provides
  public GuidePreferenceProvider provideGuidePreference() {
    return new GuidePreferenceProvider(context);
  }
}

若干もやっとしますがこういう風に使います。これでkvs-schemaとdagger2を共存させられます。

public class MainActivity extends AppCompatActivity {
  @Inject
  static GuidePreferenceProvider guidePreference;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Application.getAppComponent().inject(this);
    if(guidePreference.get().getShouldShowGuide()){
      //..
    }
    //...

コード

コード例はこちらです。

github.com

おわりに

JSR 269(Pluggable Annotation Processing API)で生成したクラスをDagger2等であつかおうとすると罠がある事がわかりました。Dagger2で触らない場合には問題にはなりません。なかなかめんどくさいですが現状は別のクラスを経由する方法をとるしかなさそうです。何か回避方法がちゃんとある気はするので知っているかた教えてください。

*1:これらのクラス名に特に意味はありません。アプリケーション全体で1つである事を表す為に用いています

ここがつらいよ realm-android 0.81.1

Realmわりといいんですが「簡単!便利!スナック!」というイメージだったのでスナックボリボリする感じで適当に書いてたらガンガンクラッシュ*1して「スナックじゃないな?ぬか漬けかな?」みたいな気持ちになりました。とりあえず一旦ぬか漬け*2部分をまとめます。将来改善予定のものもあれば、自分の設計が間違ってるんだろうなぁーというものもあります。io.realm:realm-android:0.81.1を使っての感想です。iOS版の使い心地についてはわかりません。

autoincrementがない

今のところautoincrementがないです。how to set an auto increment id? · Issue #469 · realm/realm-java · GitHubとか眺めると「まだサポートしてないぜ!」との事。将来サポートされるでしょう。とりあえず現状は以下のような仕組みをつくってしのいでます。PrimaryKeyがstringの場合は使えないです。

public class AutoIncrement {
  public static long newId(Realm realm, Class<? extends RealmObject> clazz) {
    return newIdWithIdName(realm, clazz, "id");
  }

  public static long newIdWithIdName(Realm realm, Class<? extends RealmObject> clazz, String idName) {
    return realm.where(clazz).maximumInt(idName) + 1;
  }
}

こんな感じで使ってます。

realm.beginTransaction();
Item item = realm.createObject(Item.class);
item.setId(AutoIncrement.newId(Item.class));
// 略
realm.commitTransaction();

PrimaryKeyを0で保存すると次のRealm.createObjectの時点で衝突して死ぬ

Realm.createObject(Class<E>)は指定したE型の空レコードを生成して返してくれます。この時short, int, longのフィールドは0で初期化します。すでにPrimaryKey=0のレコードが存在する場合以下のエラーを吐いて死にます。

io.realm.exceptions.RealmException: Primary key constraint broken. Value already exists: 0

まぁかならずPrimaryKeyはセットしましょうって事なんですがRealm.createObject()で死ぬので最初意味がわかりませんでした。前項のautoincrementの仕組みとか使えば問題にはならないです。

mockできない

Realmクラスはmockitoでmockできません。

Realm realm = mock(Realm.class);

Realmクラスがfinalだからです。いやーmockしたい。仕方ないのでwrapperを作ってcompositionにしました。

public class RealmWrapper {

  Realm realm;

  public RealmWrapper(Context context) {
    realm = Realm.getInstance(context);
  }

  public <E extends RealmObject> List<E> copyToRealm(Iterable<E> objects) {
    return realm.copyToRealm(objects);
  }
  //以下Realmクラスが持つpublicメソッド全てをDelegateする
}

Android StudioのGenerate-Delegate Methods...で一発でいけます。

RealmWrapper realm = mock(RealmWrapper.class);
when(realm.createObject(ItemIndex.class)).thenReturn(new ItemIndex());

やったね。まぁよく考えたらRealmに直接さわるクラスを限定してそいつをmockableにしたらいいんじゃないかな?という気もしますが。そういう意味ではActivityやFragmentやThreadで直接Realmオブジェクト触るのはあんまり良くないんじゃないかなぁという印象です。

スレッド制約

Realmはスレッドの制約がつらいです。近々マルチスレッドをサポートするそうなので期待ですね。現状なにが辛いかというと以下の点です。

  • RealmオブジェクトはgetInstance()したスレッドでしか使えない。別スレの場合そのスレッドでgetInstance()しないといけない
  • where等のRealmQueryで得たRealmResultを別のスレッドに渡せない。別スレで触ると死ぬ
  • RealmResult等から取り出したオブジェクト(いわゆるmanaged object)を別スレで触ると死ぬ

例えばItemというモデルを定義した場合以下の様なクラスが生成されます。これをmanaged objectと呼ぶらしいです。

public class ItemRealmProxy extends Item
    implements RealmObjectProxy {

Realm.createObject(Classs<E>)やクエリなどで引っ張ってきたオブジェクトの実体はこのmanaged objectになります。でこのオブジェクトは内部にRealmオブジェクトを持っていてgetter、setterを実行する際にcheckIfValid()でスレッドの検査をしている為別スレッドで触れないです。あんまり深く追っかけてないですが、スレッドの検査を通過したらnative methodを呼んでいるのでgetter、setterが呼ばれた時にはじめてDBからデータの読み書きしてるのかな?という感じです。

この制約によって以下の2つの問題と戦う事になりました。

  • 別スレで取り出したmanaged objectをUIスレッドにどうやって持ってくるか?
  • Realmオブジェクトの管理をどうするか。Dagger等でinjectionする場合スコープをどうするべきなのか?
    • ActivityにRealmをInjectすると、別スレで処理する時に死ぬ。

managed objectをUIスレッドにどうやって持ってくるか

まず問題と思ったのはmanaged objectとstandalone objectの区別が付かない点でした。

Item item = new Item(); // standalone object
Item item = realm.createObject(Item.class); //managed object

両方とも型はItemなので混乱するなぁという事でstandalone objectを持つクラスを別途用意しました。別の型である方がよいのでcompositionで作りました。最初はItemを継承したんですがこれだとコンパイルが通りませんでした*3

public class ImmutableItem {
    Item item;
    //...
}

値の初期化については最初gsonを使って以下のように書いていたんですが、

public ImmutableItem(Item item) {
  String json = gson.toJson(item);
  this.item = gson.fromJson(json, Item.class);
}

managed objectのデータアクセスはgetter、setterを通さないとダメなのでスカスカのデータにしかなりませんでした。RealmとJSONライブラリ // Speaker Deckにある通りTypeAdapterを使えばgsonでもやれるんですが意味がないので愚直に書くことにしました。

public ImmutableItem(Item item) {
  this.item = new Item();
  this.item.setId(item.getId());
  // 略
}

standalone objectである事を保証する型を作ることでAPIの返却値やObservableとかで扱いやすくなりました。

public Observable<List<ImmutableItem>> items(int page, int perPage) {
  //...
  //subscribeOn(Scheduler.io())なんかもどんとこい
}

Realmオブジェクトの管理をどうするか

RealmオブジェクトをDaggerで注入とかやってたので辛かったです。幸いRealmWrapperを作っていたのでThreadLocalを使ってゴニョゴニョする事にしました。具体的にはこんな感じ。

public class RealmWrapper {

  ThreadLocal<Realm> realms = new ThreadLocal<Realm>();

  public Realm getRealm() {
    Realm realm = realms.get();
    if (realm == null) {
      realm = Realm.getInstance(context);
      realms.set(realm);
    }
    return realm;
  }

これで触るスレッドを意識しなくてよくなるんですが思いっきりメモリリークします。この辺は後述するライフサイクル周りの話でなんとかしました。が良い方針ではないのでマネしないほうがいいと思います。

ライフサイクルがよくわからない

Realmオブジェクトってどのくらいの頻度でgetInstance()すべきなのか?ドキュメントにはonDestroy()close()しろとあるのでActivityくらいのスコープでやってたらいいのかなと思ったら当然以下のコードはダメですよね。

class AwesomeActivity exetends Activity {
  Realm realm;
  @Override
  public void onCreate(Bundle savedInstanceState) {
    realm = Realm.getInstance(this); // UI Thread
    new Thread() {
        @Override
        public void run() {
          // 異なるスレッドでrealmに触れているのでクラッシュする
          RealmResults<Item> result = realm.where(Item.class).findAll();
          List<Item> entities = toImmutableItems(result);
          //...
        }
    }.start();
  }
}

前項のThreadLocalを使ったRealmWrapperなら問題にはなりません。

class AwesomeActivity exetends Activity {
  RealmWrapper realmWrapper;
  @Override
  public void onCreate(Bundle savedInstanceState) {
    realmWrapper = RealmWrapper.getInstance(this); // UI Thread
    new Thread() {
        @Override
        public void run() {
          // 内部でThreadLocalでRealmを触るのでクラッシュしない
          RealmResults<Item> result = realmWrapper.where(Item.class).findAll();
          List<Item> entities = toImmutableItems(result);
          //...
        }
    }.start();
  }
}

しかしこの場合Threadが終了してもThreadLocalにRealmオブジェクトが保持され続けてしまいます。そこで暫定的にRealmWrapperstart(), end()ってのを生やしました。

public void start() {
  realms.set(null);
}

public void end() {
  Realm realm = getRealm();
  realm.close();
  realms.set(null);
}

とりあえずこれで動いたんですが、これは良くない設計です。確実にstart()end()を忘れるケースが出るでしょう。

//こんな感じで使う
realmWrapper.start();
RealmResults<Item> result = realmWrapper.where(Item.class).findAll();
List<Item> entities = toImmutableItems(result);
realmWrapper.end();

直接Realm.getIntance(Context)を随所に書きたくない、というのが目的なのでRealmWrapperRealmWrapper getInstanceForCurrentThread()とかを生やして使い終わったらすぐclose()ってのがまだマシな気がします。スレッド周りは制約をちゃんと加味したうえで設計していかないと辛みがあります。今回のケースは完全に失敗だったなと思います。次やるときはいい感じに設計するぞ!

json制約

「managed objectをUIスレッドにどうやって持ってくるか」の所でも書きましたがjson周りに辛みがあります。RealmとJSONライブラリ // Speaker Deckに全部書いてあります。当面はtoJson()はしない方向で頑張る方針にしました。

おわりに

いろいろ触る前にJava Docs - Realm is a mobile database: a replacement for SQLite & Core Dataを熟読するべきだったなという感じです。ゲッター/セッターメソッドは RealmObject が作るプロキシクラスによって上書きされることです。 ゲッター/セッターに書かれたロジックは実行されません。とか割と重要な事サラッと書いてある。学習した今だと色々もっと設計でカバーできそうだなぁという感じです。色々つらい目にあったけどRealm良いと思うので「私はRealmを続けるよキャンペーン」です。version見たら0.82.1が既に出ていた。差分眺めた感じ今回書いた点で変更はなさそう。

*1:使い方が間違っているとクラッシュします。良いことだと思います

*2:ぬか床の手入れって大変だそうですね

*3:A RealmClass annotated object must be derived from RealmObjectと言われます

Data Binding Libraryが吐くコードがLintに怒られる時がある

問題

booleanを使ってandroid:backgroundの値を制御するようなレイアウトを書いたとします。

<data>
  <variable name="focused" type="boolean" />
</data>
<FrameLayout
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:background="@{focused? @drawable/shape_focused : null}"
                >

この時Data Binding Libraryが以下のようなコードを吐きます。

this.mboundView1.setBackground((android.graphics.drawable.Drawable) FocusedAndroidDrawableShapeFocusedObjectnull);

アプリのminSdkVersionが16未満の場合Lintに以下の様に怒られてしまいます。

NewApi: Calling new methods on older versions

View#setBackground(Drawable)API Levelが16だからですね。@TargetApi等で分岐しろと言われます。

対応

さて、Bindingクラスは自動で生成されるので@TargetApiを追加できません。こういう時はBindingAdapterアノテーションを使います。BindingAdapterアノテーションは属性値のバインド処理を定義できるアノテーションです。どこか適当な場所(アプリケーションID.databindingなどが望ましいかもしれません)にViewGroupAdapterというクラスを作ります。

public class ViewGroupAdapter {
  @BindingAdapter("android:background")
  public static void setBackgroundAdapter(ViewGroup viewGroup, Drawable drawable) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
      setBackground(viewGroup, drawable);
    } else {
      setBackgroundDrawable(viewGroup, drawable);
    }
  }

  @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
  public static void setBackground(ViewGroup viewGroup, Drawable drawable) {
    viewGroup.setBackground(drawable);
  }

  public static void setBackgroundDrawable(ViewGroup viewGroup, Drawable drawable) {
    viewGroup.setBackgroundDrawable(drawable);
  }
}

ViewGroupAdapterは"android:background"を処理するメソッドを定義しています。対象をViewGroupにしているのでFrameLayoutにも適用されます。ImageViewなどViewGroupと継承関係のないViewへは適用されないのでご注意ください。

このクラスを定義して再度コンパイルすると生成されるコードが以下のように変化します。これでLintに怒られずにすみますね。

アプリケーションID.databinding.ViewGroupAdapter.setBackgroundAdapter(this.mboundView1, (android.graphics.drawable.Drawable) FocusedAndroidDrawableShapeFocusedObjectnull);

最後に

BindingAdapterアノテーションはアプリケーション全体に適用されるので適当にやりすぎると確実に事故ります。用法用量を守って活用しましょう。

ところでC88でTechBooster本に参加しています。Data Binding Libraryについて根掘り葉掘り書いたのでよかったら買ってください。

techbooster.github.io

Gradleで相対パスでライブラリプロジェクトを参照する

パッと出てこなかったのでメモっとく

こんな構成の時、appからlibraryを参照したいとする。

myProject
  - app
targetLibrary
  - library

myProject/settings.gradle

include 'app', ':..:targetLibrary:library'

myProject/app/build.gradle

dependencies {
    compile project(':..:targetLibrary:library')

minSdkVersion 17未満でもleanbackを使う方法

leanbackってminSdkVersion 17なので10*1とか14*2とか16*3とかしてると怒られるんですよね。

AndroidManifest.xml

<uses-sdk tools:overrideLibrary="android.support.v17.leanback"/>

OK。TV対応待ったなし。

*1:

*2:おお

*3:よしよし