今更遅れてDataBinding事始め(5.5)[コピペ用]
いままでのまとめ
- 今更遅れてDataBinding事始め(1) - exception think
- 今更遅れてDataBinding事始め(2) - exception think
- 今更遅れてDataBinding事始め(3) - exception think
- 今更遅れてDataBinding事始め(3.5) - exception think
- 今更遅れてDataBinding事始め(3.7) - exception think
- 今更遅れてDataBinding事始め(4) - exception think
- 今更遅れてDataBinding事始め(4) - exception think
- 今更遅れてDataBinding事始め(5)[コピペ用] - exception think
RecyclerView の定形記述
- 個別パーツのonClickの実装のため、View.OnClickListenerをView側に渡す //★
- 1行全体クリックのためにitemViewにOnClickListnerを設定する //◎
- 高速スクロールやBind中にbackkeyで戻るとエラーになるため、
- executePendingBindings をtry-catchで囲む
- 複雑な表示は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受信まで待たせる設計
でもユーザビリティ的に
- RecyclerViewが表示されるのに、毎回数十秒通信で待たされるのも微妙
- 通信キャッシュ+DB保存等を制御するManagerクラスを作る
という形で、
- GeocodeResponse.java
- GsonでJson MappingするGeocoderの情報を格納する為クラス
- GeoManager.java
- 通信キャッシュ+DB保存等を制御するManagerクラス
- RestUtil
- OkHttpClientの通信を呼び出すURL構築、実行クラス
- 詳細は OkHttpの非同期ラッパークラスの設計 - exception think 参照
として作成します
記述例
- GeoCoder Web API を叩いてJsonキャッシュする
- ライセンスキー無くても1日1000アクセスは使えたかと*2
- AndroidのGeoCoderクラスは使わない
- 自前でキャッシュできないから
Manager部分
- GeoManger.java
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オブジェクト部分
- org.json - How to Parse Json reponse received from Geocoding API (V3) in android application? - Stack Overflow のモデルを利用
- 生Jsonはもっておくと、Realm等で保存する時に楽になるのでそのまま //◎
- Gson対象外にするため @Expose アノテーションを付けておく
xx_xx 変数は微妙なので xxXx に置き換える。
-
- ジオコーディング | Google マップ API プログラミング解説
- Jsonの中身は結構頻繁に変わっています
geocode/GeocodeResponse.java
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; }
- layout.xml
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" app:setAddress_lat="@{model.latitude}" app:setAddress_lon="@{ model.longitude }" />