今更遅れてDataBinding事始め(5.5)[コピペ用]

いままでのまとめ


RecyclerView の定形記述

  1. 個別パーツのonClickの実装のため、View.OnClickListenerをView側に渡す //★
  2. 1行全体クリックのためにitemViewOnClickListnerを設定する //◎
  3. 高速スクロールやBind中にbackkeyで戻るとエラーになるため、
    1. executePendingBindings をtry-catchで囲む
  4. 複雑な表示はBindingAdapter関数を作って頑張る

  • layout/recycler_item.xml
<layout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" >
    <data>
        <variable name="dto" type="hoge.fuga.itemDto" />
        <variable name="listner" type="android.view.View.OnClickListener" /> //★
    </data>
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="warp_content">
        <ImageView
            android:onClick="@{listner}" //★
            android:id="@+id/icon"
            android:gravity="center_vertical|center_horizontal" 
            android:layout_weight="0"
            android:layout_width="64dp"
            android:layout_height="64dp"
            app:imageUrl="@{dto.iamge_url}" />
        <TextView 
           android:gravity="center_vertical|center_horizontal" 
           android:id="@+id/title"
           android:layout_weight="1"
           android:layout_width="0dp"
           android:layout_height="match_content"
           android:text="@{dto.title}"/>
    </LinearLayout>
</layout>
  • RecyclerViewAdapter.java
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ItemViewHolder> {
    private ArrayList<ItemDto> mItemList;
    private OnRecyclerListener mListener;

    public RecyclerViewAdapter(ArrayList<ItemDto> itemList,OnRecyclerListener listener) {
        mItemList = itemList;
        mListener = listener;
    }

    @Override
    public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // recycler_itemレイアウト
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false);
        return new ItemViewHolder(v);
    }

    @Override
    public void onBindViewHolder(ItemViewHolder holder, int position) {
       ItemDto dto = mItemList.get(position);

       View.OnClickListene listner = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mListener.onRecyclerClicked(v, i);
            }
        }

        //variable は hoge.fuga.BR にマッピングされているのでBRタグに値を設定する
        holder.getBinding().setVariable(BR.dto, dto);
        holder.getBinding().setVariable(BR.listner, listner);;
        try{
            holder.getBinding().executePendingBindings();
        }catch(Exception ex){
        }
        
        // 1行全体クリック対応 ◎
        viewHolder.itemView.setOnClickListener(listner);
    }

    public static interface OnRecyclerListener {
        void onRecyclerClicked(View v, int position);
    }
    
    // ViewHolder
    public static class ItemViewHolder extends RecyclerView.ViewHolder {
        private ViewDataBinding mBinding;
        public ItemViewHolder(View v) {
            super(v);
            mBinding = DataBindingUtil.bind(v);
        }

        public ViewDataBinding getBinding() {
            return mBinding;
        }
    }
}

通信やDB保存を隠蔽するManger

  • 最近のAPIだと
    • 画面に表示したい項目に対して情報が少ない
    • WebAPI A + WebAPI B + WebAPI C みたいな組み合わせが必須な流れになってしまうことも

だから

  • RxJavaとかでContoroler側ですべてのAPI受信まで待たせる設計
    • ビジネスロジック的には確かに美しいかも
    • 通信中インジケータがぐるぐる回りっぱなし*1
    • UIブロックが掛かるのでユーザ操作感的にかなり待たされるので微妙

でもユーザビリティ的に

  • RecyclerViewが表示されるのに、毎回数十秒通信で待たされるのも微妙
    • 通信キャッシュ+DB保存等を制御するManagerクラスを作る

という形で、

として作成します


記述例

  • GeoCoder Web API を叩いてJsonキャッシュする
    • ライセンスキー無くても1日1000アクセスは使えたかと*2
  • AndroidのGeoCoderクラスは使わない
    • 自前でキャッシュできないから

Manager部分

public class GeoManger{
    private static GeoManger instance;
    private ConcurrentHashMap<LatLng, GeocodeResponse> cashedMap = new ConcurrentHashMap<>();

    
    public static GeoManger getInstance(){
        if(instance == null){
            instance = new GeoManger();
            //[TODO]==== DBを読込する処理、過去データを消す処理等など ====
        }
        return instance;
    }

     public void getGeo(final Context context,final LatLng latlng, final Callback callback){
        if(cashedMap.containsKey(latlng)){
            GeocodeResponse geoResponse = cashedMap.get(latlng);
            callback.onSuccess(geoResponse);
            return;
        }
            
        //[TODO] OkHttpClientの通信サービス
        RestUtil.geoCoding(context, latlng, new AsyncOkHttpClient.Callback() {
            /**ジオコーディングで成功したかどうかの定数*/
            private static final String STATUS_OK = "OK";

            @Override
            public void onSuccess(okhttp3.Response response, String content) {
                Gson gson = new Gson();
                try {
                    GeocodeResponse geoResponse = gson.fromJson(content, GeocodeResponse.class);
                    if (TextUtils.equals(geoResponse.getStatus(), STATUS_OK)) {
                        geoResponse.setContent(content);   // ◎ 生Jsonを保存 
                        cashedMap.putIfAbsent(latlng,geoResponse); //メモリ城のマップに保存
                        //[TODO] ====DB追加コードなど====
                        callback.onSuccess(geoResponse);
                    }
                    else{
                        callback.onError(null);
                    }
                } catch (JsonSyntaxException e) {
                    callback.onError(e);
                }
            }

            @Override
            public void onFailure(okhttp3.Response response, Throwable throwable) {
                callback.onError(throwable);
            }
        });
    }
    
    public static interface Callback(){
        void onSuccess(GeocodeResponse geoResponse);
        void onError(Throwable throwable);
    }

}

RestUtil部分

public class RestUtil{

    //URLのSCHEMEの定数
    public static String SCHEME_HTTPS = "https";
    public static String SCHEME_HTTP  = "http";

    public static String HOST_GOOGLE = "maps.google.com";
    public static String PATH_SEGMENT_GOOGLE_MAPS = "maps",
    public static String PATH_SEGMENT_GOOGLE_API = "api",
    public static String PATH_SEGMENT_GOOGLE_GEOCODE = "geocode",
    public static String PATH_SEGMENT_GOOGLE_JSON = "json";

      // Geo座標検索
      public static void getGeo(Context context, LatLng latlng, AsyncOkHttpClient.Callback callback) {
        //HttpUrlの生成
        final HttpUrl httpUrl = new HttpUrl.Builder()
                .scheme(SCHEME_HTTP)
                .host(HOST_GOOGLE)
                .addPathSegment(PATH_SEGMENT_GOOGLE_MAPS)
                .addPathSegment(PATH_SEGMENT_GOOGLE_API)
                .addPathSegment(PATH_SEGMENT_GOOGLE_GEOCODE)
                .addPathSegment(PATH_SEGMENT_GOOGLE_JSON)
                .addQueryParameter("latlng", "" + latlng.latitude +"," + latlng.longitude)
                .addQueryParameter("language", Locale.getDefault().getCountry().toLowerCase())
                .addQueryParameter("region", Locale.getDefault().getCountry().toLowerCase())
                .addQueryParameter("sensor","false")
                .build();

        //通信開始
        new RestService(context).get(httpUrl, null, callback);
    }

     // Geo住所検索
    public static void getGeo(Context context, String address, AsyncOkHttpClient.Callback callback) {
        //HttpUrlの生成
        final HttpUrl httpUrl = new HttpUrl.Builder()
                .scheme(SCHEME_HTTP)
                .host(HOST_GOOGLE)
                .addPathSegment(PATH_SEGMENT_GOOGLE_MAPS)
                .addPathSegment(PATH_SEGMENT_GOOGLE_API)
                .addPathSegment(PATH_SEGMENT_GOOGLE_GEOCODE)
                .addPathSegment(PATH_SEGMENT_GOOGLE_JSON)
                .addQueryParameter("address", address)
                .addQueryParameter("sensor","false")
                .build();

        //通信開始
        new RestService(context).get(httpUrl, null, callback);
    }

Gsonオブジェクト部分

public class GeocodeResponse {
    private String status;
    private List<Result> results;;

    //生JSON ◎
    @Expose
    private String content;

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public void setResults(List<Result> results) {
        this.results = results;
    }

    public List<Result> getResults() {
        return results;
    }
    
    public void setContent(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}
  • geocode/Result.java
public class Result {
    @SerializedName("formatted_address") //★
    private String formattedAddress;
    
    @SerializedName("address_components") //★
    private List<AddressComponent> addressComponents;
    
    private Geometry geometry;
    
    public void setFormattedAddress(String formattedAddress) {
        this.formattedAddress = formattedAddress;
    }

    public String getFormattedAddress() {
        return formattedAddress;
    }
    public void setAddress_components(List<AddressComponent> addressComponents)  {   
       this.addressComponents = addressComponents;
    }

    public Collection<AddressComponent> getAddress_components() {
        return address_components;
    }

    public Geometry getGeometry() {
        return geometry;
    }

    public void setGeometry(Geometry geometry) {
        this.geometry = geometry;
    }
}
  • geocode/AddressComponent.java
public class AddressComponent {
    @SerializedName("long_name")
    private String longName;
    
    @SerializedName("short_name")
    private String shortName;
    
    private List<String> types;

    public String getLongName() {
        return longName;
    }

    public void setLongName(String longName) {
        this.longName = longName;
    }

    public String getShortName() {
        return shortName;
    }

    public void setShortName(String shortName) {
        this.shortName = shortName;
    }

    public List<String> getTypes() {
        return types;
    }

    public void setTypes(List<String> types) {
        this.types = types;
    }
}
  • geocode/Geometry.java
public class Geometry {
    private Location location;

    public Location getLocation() {
        return location;
    }

    public void setLocation(Location location) {
        this.location = location;
    }

}
  • geocode/Location.java
public class Location {
    private double lat;
    private double lng;

    public void setLat(double lat) {
        this.lat = lat;
    }

    public double getLat() {
        return lat;
    }

    public void setLng(double lng) {
        this.lng = lng;
    }

    public double getLng() {
        return lng;
    }
}

BindingAdapter での利用例

  • CommonAdapter.java
@BindingAdapter(value = {
            "setAddress_lat",
            "setAddress_lon"
    },requireAll = false)
    public static void setAddress(final TextView view,double latitude,double longitude) {
        final LatLng latlng = new LatLng(latitude,longitude);
        Context context = view.getContext();

        GeoManager.getInstance().getGeo(context, latlng, new GeoManager.Callback() {
            @Override
            public void onSuccess(GeocodeResponse geoResponse) {

                        String sAddress = "";
                        List<AddressComponent> addressComponents = geoResponse.getResults().get(0).getAddressComponents();

                        for (AddressComponent addressComponent : addressComponents) {
                            String name = addressComponent.getLongName();
                            if(containsUnicode(name)){
                                sAddress = name;
                                break;
                            }
                            name = addressComponent.getShortName();
                            if(containsUnicode(name)){
                                sAddress = name;
                                break;
                            }
                        }
                        if(TextUtils.isEmpty(sAddress)){
                            sAddress = geoResponse.getResults().get(0).getFormattedAddress();
                        }

                        view.setText(sAddress);
            }

            @Override
            public void onError(Throwable throwable) {

            }
        });
    }

    // 日本語判定
    public static boolean containsUnicode(String str) {
        for(int i = 0 ; i < str.length() ; i++) {
            char ch = str.charAt(i);
            Character.UnicodeBlock unicodeBlock = Character.UnicodeBlock.of(ch);

            if (Character.UnicodeBlock.HIRAGANA.equals(unicodeBlock))
                return true;

            if (Character.UnicodeBlock.KATAKANA.equals(unicodeBlock))
                return true;

            if (Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS.equals(unicodeBlock))
                return true;

            if (Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS.equals(unicodeBlock))
                return true;

            if (Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION.equals(unicodeBlock))
                return true;
        }
        return false;
    }
<TextView 
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:setAddress_lat="@{model.latitude}" 
    app:setAddress_lon="@{ model.longitude }"
/>

*1:WebAPI A => C すべて終わるまで表示処理に行かない

*2:Place APIだとキーは必須