2016年3月16日水曜日

Migrate from Retrofit to Retrofit2 (Retrofit から Retrofit2 に移行する)

retrofit2 が正式リリースされました。

retrofit/CHANGELOG.md at master · square/retrofit

retrofit との後方互換性はありません。そのため maven の group id が com.squareup.retrofit2 になっています。
  1. compile 'com.squareup.retrofit2:retrofit:2.0.0'  

Converter

GSON などの Converter は別のモジュールに分割されました。

retrofit
  1. new RestAdapter.Builder()  
  2.     .setConverter(new GsonConverter(gson))  
  3.     ...  
retrofit2
  1. compile 'com.squareup.retrofit2:converter-gson:2.0.0'  
  1. new Retrofit.Builder()  
  2.     .addConverterFactory(GsonConverterFactory.create(gson))  
  3.     ...  
gson 以外の Converter については square.github.io/retrofit/ の CONVERTERS 部分に記載があります。


RxJava

retrofit で RxJava を使うためのクラスは別のモジュールに分割されました。

retrofit2
  1. compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0'  
  1. new Retrofit.Builder()  
  2.     .addCallAdapterFactory(RxJavaCallAdapterFactory.create())  
  3.     ...  

AndroidLog

retrofit.android.AndroidLog はなくなりました。 https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor
を使って okhttp3 の interceptor で行います。

retrofit
  1. AndroidLog logger = new AndroidLog(MyService.class.getSimpleName());  
  2. new RestAdapter.Builder()  
  3.     .setLogLevel(RestAdapter.LogLevel.FULL)  
  4.     .setLog(logger)  
  5.     ...  
retrofit2
  1. compile 'com.squareup.okhttp3:logging-interceptor:3.0.0'  
  1. HttpLoggingInterceptor logging = new HttpLoggingInterceptor();  
  2. logging.setLevel(HttpLoggingInterceptor.Level.BODY);  
  3. new OkHttpClient.Builder();  
  4.     .addInterceptor(logging)  
  5.     ...  

ErrorHandler, RetrofitError

retrofit.ErrorHandler, retrofit.RetrofitError はなくなりました。
okhttp3 の interceptor で行うとか https://github.com/square/retrofit/issues/1102
CallAdapterFactory を作るとか https://github.com/square/retrofit/pull/1277/files
いくつか方法があります。

adapter-rxjava を使っているなら HttpException が onError() で返されるので、ここから status code, message, response が取れます。

retrofit
  1. new RestAdapter.Builder()  
  2.     .setErrorHandler(new MyErrorHandler())  
  3.     ...  
  1. public class MyErrorHandler implements ErrorHandler {  
  2.     @Override  
  3.     public Throwable handleError(RetrofitError cause) {  
  4.         if (cause.getKind() == RetrofitError.Kind.NETWORK) {    
  5.             // network error  
  6.             ...  
  7.         } else if (cause.getResponse() != null) {    
  8.             final int statusCode = cause.getResponse().getStatus();    
  9.             try {    
  10.                 MyErrorResponse errorResponse =  
  11.                     (MyErrorResponse) cause.getBodyAs(MyErrorResponse.class);    
  12.             } catch (Exception e) {    
  13.                 ...  
  14.             }    
  15.         } else {  
  16.             ...  
  17.         }  
  18.     }  
  19. }  
retrofit2
  1.     @Override  
  2.     public void onError(Throwable e) {  
  3.         if (e instanceof IOException) {  
  4.             // network error  
  5.             ...  
  6.         } else if (e instanceof HttpException) {  
  7.             HttpException he = (HttpException) e;  
  8.             final int statusCode = he.code();  
  9.             final ResponseBody errorBody = e.response().errorBody();  
  10.             if (errorBody != null) {  
  11.                 try {  
  12.                     MyErrorResponse errorResponse =   
  13.                         (MyErrorResponse) GsonConverterFactory  
  14.                             .create()  
  15.                             .responseBodyConverter(MyErrorResponse.class,   
  16.                                 new Annotation[0], null)  
  17.                             .convert(errorBody);  
  18.    
  19.                 // retrofit インスタンスが利用できる場合  
  20. //                MyErrorResponse errorResponse = retrofit  
  21. //                        .responseConverter(MyErrorResponse.class, new Annotation[0])  
  22. //                        .convert(errorBody);  
  23.   
  24.                 } catch (Exception re) {  
  25.                     ...  
  26.                 }  
  27.             }  
  28.         } else {  
  29.             ...  
  30.         }  
  31.     }  

RequestInterceptor

retrofit.RequestInterceptor はなくなりました。
okhttp3 の interceptor を使います。https://github.com/square/retrofit/issues/1082

retrofit
  1. public class MyRequestInterceptor implements RequestInterceptor {  
  2.     @Override  
  3.     public void intercept(RequestFacade request) {  
  4.         request.addHeader(HEADER_KEY, headerValue);  
  5.     }  
  6. }  
  1. new RestAdapter.Builder()  
  2.     .setRequestInterceptor(new MyRequestInterceptor())  
  3.     ...  
retrofit2
  1. public class MyRequestInterceptor implements Interceptor {  
  2.     @Override  
  3.     public Response intercept(Chain chain) throws IOException {  
  4.         final Request.Builder builder = chain.request().newBuilder();  
  5.         builder.addHeader(HEADER_KEY, headerValue);  
  6.         return chain.proceed(builder.build());  
  7.     }  
  8. }  
  1. new OkHttpClient.Builder()  
  2.     .addInterceptor(new MyRequestInterceptor())  
  3.     ...  

Response

import 先を変更する必要があります。

retrofit
  1. import retrofit.client.Response  
retrofit2
  1. import retrofit2.Response  

Observable<Response>

ResponseBody を指定する必要があります。

retrofit
  1. @DELETE("/delete")  
  2. Observable<Response> delete();  
retrofit2
  1. @DELETE("/delete")  
  2. Observable<Response<ResponseBody>> delete();  

@Body, @GET, @Post など

import 先を変更する必要があります。

retrofit
  1. import retrofit.http.Body  
retrofit2
  1. import retrofit2.http.Body  
Query の encodeValue は encoded に名前が変わっています。

retrofit
  1. @Query(value = "categories", encodeValue = false@Nullable String categories  
retrofit2
  1. @Query(value = "categories", encoded = false@Nullable String categories  

TypedFile

TypedFile はなくなりました。
MultipartBody.Part を使います。

retrofit
  1. @Multipart  
  2. @POST("/upload")  
  3. Observable<Response> uploadImage(  
  4.         @Part("image") TypedFile image  
  5. );  
  1. final TypedFile image = new TypedFile("image/jpeg", file);  
retrofit2
  1. @Multipart  
  2. @PATCH("/upload")  
  3. Observable<Response> uploadImage(  
  4.         @Part() MultipartBody.Part image  
  5. );  
MultipartBody.Part のときは @Part に value(上記だと "image")を設定してはいけません。設定すると実行時に落ちます。
  1. final MultipartBody.Part image = MultipartBody.Part.createFormData("image",  
  2.         file.getName(),   
  3.         RequestBody.create(MediaType.parse("image/jpeg"), file));  
もともと @Part に指定していた "image" は MultipartBody.Part.createFormData() の第1引数に指定します。


Client

retrofit.client.Client はなくなりました。

retrofit側でモック化したい場合は retrofit-mock があります。
  1. compile 'com.squareup.retrofit2:retrofit-mock:2.0.0'  
使い方は
https://github.com/square/retrofit/blob/master/samples/src/main/java/com/example/retrofit/SimpleMockService.java が参考になります。

okhttp側でモック化したい場合は mockwebserverがあります。
  1. compile 'com.squareup.okhttp3:mockwebserver:3.0.0'  



okhttp

okhttp3 を使います。

retrofit
  1. import com.squareup.okhttp.Interceptor;    
  2. import com.squareup.okhttp.Request;    
  3. import com.squareup.okhttp.Response;  
retrofit2
  1. import okhttp3.Interceptor;  
  2. import okhttp3.Request;  
  3. import okhttp3.Response;  

stetho-okhttp

stetho-okhttp3 を使います。https://github.com/facebook/stetho/issues/327

retrofit2
  1. compile 'com.facebook.stetho:stetho-okhttp3:1.3.1'  



java.io.InterruptedIOException: thread interrupted

Retrofit2 (というか okhttp3)にして
  1. thread interrupted  
  2. java.io.InterruptedIOException: thread interrupted  
  3. at okio.Timeout.throwIfReached(Timeout.java:145)  
  4. at okio.Okio$1.write(Okio.java:77)  
  5. ...  
というようなエラーが起きたら、RxJava の subscribeOn() や observeOn() の使い方があやしいので見直しましょう。
例えば以下のような、subscribeOn(Schedulers.newThread()) を指定した複数の Observable を merge して takeLast すると InterruptedIOExceptionになります。 zip や combineLatest だと問題ないです。

NG
  1. final List<Observable<ImageResponse>> observables = new ArrayList<>();  
  2. for (...) {  
  3.     final MultipartBody.Part image = ...;  
  4.     final Observable<ImageResponse> o = service.uploadImage(image)  
  5.             .subscribeOn(Schedulers.newThread());  
  6.     observables.add(o);  
  7. }  
  8.   
  9. Observable  
  10.         .merge(observables)  
  11.         .takeLast(1)  
  12.         .subscribeOn(Schedulers.newThread());  
  13.         .observeOn(AndroidSchedulers.mainThread())  
  14.         .subscribe(new Subscriber<ImageResponse>() {  
  15.                 ...  
  16.         });  
OK
  1. final List<Observable<ImageResponse>> observables = new ArrayList<>();  
  2. for (...) {  
  3.     final MultipartBody.Part image = ...;  
  4.     final Observable<ImageResponse> o = service.uploadImage(image)  
  5.             .subscribeOn(Schedulers.newThread());  
  6.     observables.add(o);  
  7. }  
  8.   
  9. Observable  
  10.         .combineLatest(observables, new FuncN<ImageResponse>() {  
  11.                 @Override  
  12.                 public ImageResponse call(Object... args) {  
  13.                     return ...;  
  14.                 }  
  15.         })  
  16.         .subscribeOn(Schedulers.newThread());  
  17.         .observeOn(AndroidSchedulers.mainThread())  
  18.         .subscribe(new Subscriber<ImageResponse>() {  
  19.                 ...  
  20.         });  



2016年3月6日日曜日

RecyclerView で IndexOutOfBoundsException が起こる話

注意:以下の現象は support library v23.2.0 では起こりません。

support library v23.1.1 で以下のコードを実行すると落ちます。

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 0(offset:20).state:20 at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4405) ...
  1. public class RecyclerViewGoneActivity extends AppCompatActivity {  
  2.   
  3.     private TextAdapter textAdapter;  
  4.   
  5.     @Override  
  6.     protected void onCreate(Bundle savedInstanceState) {  
  7.         super.onCreate(savedInstanceState);  
  8.         setContentView(R.layout.activity_recycler_view_gone);  
  9.   
  10.         final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);  
  11.         recyclerView.setLayoutManager(new LinearLayoutManager(this));  
  12.   
  13.         // 1. 初期状態がGONE  
  14.         recyclerView.setVisibility(View.GONE);  
  15.   
  16.         // 2. setHasFixedSize(true);  
  17.         recyclerView.setHasFixedSize(true);  
  18.   
  19.         textAdapter = new TextAdapter();  
  20.         recyclerView.setAdapter(textAdapter);  
  21.   
  22.         new Handler().postDelayed(new Runnable() {  
  23.             @Override  
  24.             public void run() {  
  25.                 ArrayList<String> data = new ArrayList<>();  
  26.                 for (int i = 0; i < 20; i++) {  
  27.                     data.add("data : " + i);  
  28.                 }  
  29.                 // 3. notifyItemRangeInserted(); を呼ぶ  
  30.                 textAdapter.addAll(data);  
  31.                 // 4. visibility を VISIBLE にする  
  32.                 recyclerView.setVisibility(View.VISIBLE);  
  33.             }  
  34.         }, 1000);  
  35.     }  
  36.   
  37.     private static class TextAdapter extends RecyclerView.Adapter<ViewHolder> {  
  38.   
  39.         private final ArrayList<String> data = new ArrayList<>();  
  40.   
  41.         @Override  
  42.         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {  
  43.             LayoutInflater inflater = LayoutInflater.from(parent.getContext());  
  44.             return new ViewHolder(inflater.inflate(ViewHolder.LAYOUT_ID, parent, false));  
  45.         }  
  46.   
  47.         @Override  
  48.         public void onBindViewHolder(ViewHolder holder, int position) {  
  49.             holder.textView.setText(data.get(position));  
  50.         }  
  51.   
  52.         @Override  
  53.         public int getItemCount() {  
  54.             return data.size();  
  55.         }  
  56.   
  57.         public void add(String s) {  
  58.             final int positionStart = getItemCount();  
  59.             data.add(s);  
  60.             notifyItemInserted(positionStart);  
  61.         }  
  62.   
  63.         public void addAll(ArrayList<String> d) {  
  64.             final int positionStart = getItemCount();  
  65.             final int itemCount = d.size();  
  66.             data.addAll(d);  
  67.             notifyItemRangeInserted(positionStart, itemCount);  
  68.         }  
  69.   
  70.         public void clear() {  
  71.             final int itemCount = getItemCount();  
  72.             data.clear();  
  73.             notifyItemRangeRemoved(0, itemCount);  
  74.         }  
  75.     }  
  76.   
  77.     private static class ViewHolder extends RecyclerView.ViewHolder {  
  78.   
  79.         public static final int LAYOUT_ID = android.R.layout.simple_list_item_1;  
  80.   
  81.         final TextView textView;  
  82.   
  83.         public ViewHolder(View itemView) {  
  84.             super(itemView);  
  85.             this.textView = (TextView) itemView.findViewById(android.R.id.text1);  
  86.         }  
  87.     }  
  88. }  



落ちなくする方法がいくつかあります。
  • 1. setHasFixedSize(true); を呼ばないようにする(ただしその分パフォーマンスに影響する)
  • 2. あらかじめデータを入れておいて VISIBLE にする前に消す
    1. textAdapter = new TextAdapter();  
    2. recyclerView.setAdapter(textAdapter);  
    3.   
    4. textAdapter.add("dummy"); // これを追加  
    5.   
    6. new Handler().postDelayed(new Runnable() {  
    7.     @Override  
    8.     public void run() {  
    9.         textAdapter.clear(); // これを追加  
    10.         ArrayList<String> data = new ArrayList<>();  
    11.         for (int i = 0; i < 20; i++) {  
    12.             data.add("data : " + i);  
    13.         }  
    14.         textAdapter.addAll(data);  
    15.         recyclerView.setVisibility(View.VISIBLE);  
    16.     }  
    17. }, 1000);  
関連


2016年3月5日土曜日

CollapsingToolbarLayout で status bar を透明にする方法

注意:以下は support library v23.2.0 での動作をもとにしています。

追記
AppBarLayout と CollapsingToolbarLayout にも android:fitsSystemWindows="true" の指定を追加するようにしました。 これにより、v25.0.1, v25.0,0, v24.2.1, v24.2.0, v24.1.1, v24.1.0, v24.0.0, v23.4.0, v23.3.0, v23.2.1 でも動作することを確認してあります。



わかりやすいように

colorPrimary : #ff0000(赤)
colorPrimaryDark : #99ff00ff(マゼンダ)

contentScrim : #990000ff(青)
statusBarScrim : #9900ffff(シアン)

Toolbar の background : #9900ff00(緑)

にしてあります。

普通に実装するとこんな感じになります。
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     xmlns:app="http://schemas.android.com/apk/res-auto"  
  4.     xmlns:tools="http://schemas.android.com/tools"  
  5.     android:layout_width="match_parent"  
  6.     android:layout_height="match_parent">  
  7.   
  8.     <android.support.design.widget.AppBarLayout  
  9.         android:layout_width="match_parent"  
  10.         android:layout_height="wrap_content"  
  11.         android:fitsSystemWindows="true"  
  12.         android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">  
  13.   
  14.         <android.support.design.widget.CollapsingToolbarLayout  
  15.             android:id="@+id/toolbar_layout"  
  16.             android:layout_width="match_parent"  
  17.             android:layout_height="wrap_content"  
  18.             android:fitsSystemWindows="true"  
  19.             app:contentScrim="#990000ff"  
  20.             app:layout_scrollFlags="scroll|exitUntilCollapsed"  
  21.             app:statusBarScrim="#9900ffff">  
  22.   
  23.             <ImageView  
  24.                 android:layout_width="match_parent"  
  25.                 android:layout_height="360dp"  
  26.                 android:scaleType="centerCrop"  
  27.                 android:src="@drawable/sample"  
  28.                 tools:ignore="ContentDescription" />  
  29.   
  30.             <android.support.v7.widget.Toolbar  
  31.                 android:id="@id/toolbar"  
  32.                 android:layout_width="match_parent"  
  33.                 android:layout_height="?attr/actionBarSize"  
  34.                 app:layout_collapseMode="pin"  
  35.                 tools:background="#9900ff00" />  
  36.   
  37.         </android.support.design.widget.CollapsingToolbarLayout>  
  38.   
  39.     </android.support.design.widget.AppBarLayout>  
  40.   
  41.     <android.support.v4.widget.NestedScrollView  
  42.         android:layout_width="match_parent"  
  43.         android:layout_height="match_parent"  
  44.         android:layout_gravity="fill_vertical"  
  45.         app:layout_behavior="@string/appbar_scrolling_view_behavior">  
  46.   
  47.         ...  
  48.   
  49.     </android.support.v4.widget.NestedScrollView>  
  50.   
  51. </android.support.design.widget.CoordinatorLayout>  
これを実行するとこうなります。



マゼンダ色の status bar が表示されています。この status bar を透明にして ImageView をその分上にあげるにはどうすればいいか。

そのためにまずテーマに
  1. <item name="android:statusBarColor">@android:color/transparent</item>  
を指定して、Activity の onCreate() に以下の処理を追加します。
  1. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {    
  2.     findViewById(android.R.id.content).setSystemUiVisibility(    
  3.             View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);    
  4. }   
すると次のようになります。



マゼンダから赤色に変わりました。これはステータスバーの色が見えているのではなく CollapsingToolbar の領域が見えています。その証拠にスクロールするとこの領域も移動します。

この領域はどこで確保されているかというと CollapsingToolbarLayout の onLayout() です。
  1. @Override  
  2. protected void onLayout(boolean changed, int left, int top, int right, int bottom) {  
  3.     super.onLayout(changed, left, top, right, bottom);  
  4.   
  5.     ...  
  6.   
  7.     // Update our child view offset helpers  
  8.     for (int i = 0, z = getChildCount(); i < z; i++) {  
  9.         final View child = getChildAt(i);  
  10.   
  11.         if (mLastInsets != null && !ViewCompat.getFitsSystemWindows(child)) {  
  12.             final int insetTop = mLastInsets.getSystemWindowInsetTop();  
  13.             if (child.getTop() < insetTop) {  
  14.                 // If the child isn't set to fit system windows but is drawing within the inset  
  15.                 // offset it down  
  16.                 ViewCompat.offsetTopAndBottom(child, insetTop);  
  17.             }  
  18.         }  
  19.   
  20.         getViewOffsetHelper(child).onViewLayout();  
  21.     }  
  22.   
  23.     ...  
  24. }  
CollapsingToolbarLayout の直接の子ビューで fitsSystemWindows が false のものは、ビューの配置位置を insetTop(ステータスバーの高さ)分だけ下にずらすようになっています。
つまり、fitsSystemWindows = true を子ビューにセットすれば、この処理が行われないということです。

そこで ImageView に android:fitsSystemWindows="true" を追加すると
  1. <android.support.design.widget.CollapsingToolbarLayout  
  2.     ... >  
  3.   
  4.     <ImageView  
  5.         ...  
  6.         android:fitsSystemWindows="true" />  
  7.   
  8.     <android.support.v7.widget.Toolbar  
  9.         ... />  
  10.   
  11. </android.support.design.widget.CollapsingToolbarLayout>  
次のようになります。




この方法を取る場合、4.4 で注意が必要です。 「 StatusBar 透明化の正しい方法」で書いているように、v19 では android:windowTranslucentStatus をセットして、v21 では android:statusBarColor をセットしている場合、上記の対応を入れると次のように 4.4 で余分な領域が確保されてしまいます。



そのため、v21以降だけ android:fitsSystemWindows="true" が指定されるようにします。

values/bools.xml
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <resources>  
  3.     <bool name="fitsSystemWindowForImage">false</bool>  
  4. </resources>  
values-v21/bools.xml
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <resources>  
  3.     <bool name="fitsSystemWindowForImage">true</bool>  
  4. </resources>  
  1. <android.support.design.widget.CollapsingToolbarLayout  
  2.     ... >  
  3.   
  4.     <ImageView  
  5.         ...  
  6.         android:fitsSystemWindows="@bool/fitsSystemWindowForImage" />  
  7.   
  8.     <android.support.v7.widget.Toolbar  
  9.         ... />  
  10.   
  11. </android.support.design.widget.CollapsingToolbarLayout>  
こうすると、4.4 でも次のような結果になります。




4.4 での実行結果をよく見ると、閉じたときに status bar 分の領域が確保されずに Toolbar が status bar の下にきてしまうことがわかります。



残念ながら 4.4 で閉じた時に status bar 分を確保するシンプルな方法はありません。 以下のように inset を margin に付け替える Toolbar を用意するとこれが解決できます。
  1. public class CustomToolbar extends Toolbar {  
  2.   
  3.     public CustomToolbar(Context context) {  
  4.         super(context);  
  5.     }  
  6.   
  7.     public CustomToolbar(Context context, AttributeSet attrs) {  
  8.         super(context, attrs);  
  9.     }  
  10.   
  11.     public CustomToolbar(Context context, AttributeSet attrs, int defStyleAttr) {  
  12.         super(context, attrs, defStyleAttr);  
  13.     }  
  14.   
  15.     @Override  
  16.     protected boolean fitSystemWindows(Rect insets) {  
  17.         final ViewGroup.LayoutParams params = getLayoutParams();  
  18.         if (params instanceof MarginLayoutParams) {  
  19.             ((MarginLayoutParams) params).topMargin = insets.top;  
  20.         }  
  21.         return true;  
  22.     }  
  23. }