Retrofit2の仕組み 〜オリジナルConverterを作ってみる〜 / How Retrofit2 works ~ Try making your own Converter ~

Retrofit2の仕組み 〜オリジナルConverterを作ってみる〜 / How Retrofit2 works ~ Try making your own Converter ~

Android研究&発表会#2で発表した資料です!
https://arap.connpass.com/event/148500/

サンプル
https://github.com/kwmt/RetrofitSample

Kotlin Serialization Converter
https://github.com/JakeWharton/retrofit2-kotlinx-serialization-converter

Ce6acc3536b0e0340b5f0569d3394c9c?s=128

Yasutaka Kawamoto

October 26, 2019
Tweet

Transcript

  1. Retrofit2ͷ࢓૊Έ Androidݚڀ&ൃදձ #2 2019/10/26 ʙΦϦδφϧConverterΛ࡞ͬͯΈΔʙ

  2. • Տຊ ହ޹(͔Θ΋ͱ ΍͔ͨ͢) • ॴଐɿגࣜձࣾ tech vein 
 (େࡕࢢதԝ۠ຊொ)

    • ϞόΠϧΞϓϦΤϯδχΞ
 (AndroidଟΊɺiOS΋ŧŔŕŪũƄŝſ) • GitHub: kwmt ɺtwitter: kwmt27 • Google I/O2018 ॳࢀՃ • ෱Ԭग़਎ • ΫϥϑτϏʔϧ޷͖ 2 ࣗݾ঺հ
  3. • Squareࣾ੡ • JavaϥΠϒϥϦ • HTTPΫϥΠΞϯτ • Android or JavaͰॻ͔ΕͨαʔόʔͰಈ͘

    Retrofitͱ͸
  4. interface ApiService { } RetrofitͷΞΠσΞ

  5. interface ApiService { } RetrofitͷΞΠσΞ class HttpGitHubService : ApiService {

    // … }
  6. interface ApiService { } RetrofitͷΞΠσΞ

  7. None
  8. implementation 'com.squareup.retrofit2:retrofit:2.6.2'

  9. interface GitHubService { fun listRepos(user: String): List<Repo> }

  10. interface GitHubService { fun listRepos(user: String): List<Repo> } data class

    Repo(val name: String)
  11. interface GitHubService { fun listRepos(user: String): List<Repo> }

  12. interface GitHubService { @GET("users/{user}/repos") fun listRepos(user: String): List<Repo> }

  13. interface GitHubService { @GET("users/{user}/repos") fun listRepos(@Path("user") user: String): List<Repo> }

  14. interface GitHubService { @GET("users/{user}/repos") fun listRepos(@Path("user") user: String): Call<List<Repo>> }

  15. val retrofit: Retrofit = Retrofit.Builder() .baseUrl("https://api.github.com/") .build()

  16. val retrofit: Retrofit = Retrofit.Builder() .baseUrl("https://api.github.com/") .build() val gitHubService: GitHubService

    
 = retrofit.create(GitHubService::class.java)
  17. val repos: Call<List<Repo>> = gitHubService.listRepos("kwmt")

  18. val repos: Call<List<Repo>> = gitHubService.listRepos("kwmt") repos.enqueue(object : retrofit2.Callback<List<Repo>> { override

    fun onFailure(call: Call<List<Repo>>, t: Throwable) { Log.d("GitHubSample", t.message) } override fun onResponse( call: Call<List<Repo>>, response: Response<List<Repo>>) { Log.d("GitHubSample", response.toString()) } })
  19. Caused by: java.lang.IllegalArgumentException: 
 Could not locate ResponseBody converter for


    java.util.List<net.kwmt27.retrofitsample.data.model.Repo>.
  20. Ϋϥογϡ͠·͢ɻ Caused by: java.lang.IllegalArgumentException: 
 Could not locate ResponseBody converter

    for
 java.util.List<net.kwmt27.retrofitsample.data.model.Repo>.
  21. Ϋϥογϡ͠·͢ɻ Caused by: java.lang.IllegalArgumentException: 
 Could not locate ResponseBody converter

    for
 java.util.List<net.kwmt27.retrofitsample.data.model.Repo>. ͳΜ͔ConverterབྷΈͰౖΒΕͯΔ༧૝͸͖ͭ·͕͢ɺ
  22. Ϋϥογϡ͠·͢ɻ Caused by: java.lang.IllegalArgumentException: 
 Could not locate ResponseBody converter

    for
 java.util.List<net.kwmt27.retrofitsample.data.model.Repo>. val repos: Call<List<Repo>> = gitHubService.listRepos("kwmt") ͳΜ͔ConverterབྷΈͰౖΒΕͯΔ༧૝͸͖ͭ·͕͢ɺ
  23. Ϋϥογϡ͠·͢ɻ Caused by: java.lang.IllegalArgumentException: 
 Could not locate ResponseBody converter

    for
 java.util.List<net.kwmt27.retrofitsample.data.model.Repo>. val repos: Call<List<Repo>> = gitHubService.listRepos("kwmt") ͜͜ͰΫϥογϡ͍ͯ͠ΔΑ͏ͳͷͰɺਂ͘ݟͯΈ·͢ɻ ͳΜ͔ConverterབྷΈͰౖΒΕͯΔ༧૝͸͖ͭ·͕͢ɺ
  24. retrofit.create(GitHubService::class.java) public <T> T create(final Class<T> service) { ɹ// লུ

    return (T) Proxy.newProxyInstance( service.getClassLoader(), new Class<?>[] { service }, new InvocationHandler() { @Override public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable { ɹɹɹɹɹ // লུ return loadServiceMethod(method) .invoke(args != null ? args : emptyArgs); } }); }
  25. retrofit.create(GitHubService::class.java) public <T> T create(final Class<T> service) { ɹ// লུ

    return (T) Proxy.newProxyInstance( service.getClassLoader(), new Class<?>[] { service }, new InvocationHandler() { @Override public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable { ɹɹɹɹɹ // লུ return loadServiceMethod(method) .invoke(args != null ? args : emptyArgs); } }); }
  26. retrofit.create(GitHubService::class.java) public <T> T create(final Class<T> service) { ɹ// লུ

    return (T) Proxy.newProxyInstance( service.getClassLoader(), new Class<?>[] { service }, new InvocationHandler() { @Override public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable { ɹɹɹɹɹ // লུ return loadServiceMethod(method) .invoke(args != null ? args : emptyArgs); } }); }
  27. if (annotation instanceof DELETE) { parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false); }

    else if (annotation instanceof GET) { parseHttpMethodAndPath("GET", ((GET) annotation).value(), false); } else if (annotation instanceof HEAD) { parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false); } else if (annotation instanceof PATCH) { parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true); } else if (annotation instanceof POST) { parseHttpMethodAndPath("POST", ((POST) annotation).value(), true); } else if (annotation instanceof PUT) { parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
  28. if (annotation instanceof DELETE) { parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false); }

    else if (annotation instanceof GET) { parseHttpMethodAndPath("GET", ((GET) annotation).value(), false); } else if (annotation instanceof HEAD) { parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false); } else if (annotation instanceof PATCH) { parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true); } else if (annotation instanceof POST) { parseHttpMethodAndPath("POST", ((POST) annotation).value(), true); } else if (annotation instanceof PUT) { parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
  29. if (annotation instanceof DELETE) { parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false); }

    else if (annotation instanceof GET) { parseHttpMethodAndPath("GET", ((GET) annotation).value(), false); } else if (annotation instanceof HEAD) { parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false); } else if (annotation instanceof PATCH) { parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true); } else if (annotation instanceof POST) { parseHttpMethodAndPath("POST", ((POST) annotation).value(), true); } else if (annotation instanceof PUT) { parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
  30. if (annotation instanceof DELETE) { parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false); }

    else if (annotation instanceof GET) { parseHttpMethodAndPath("GET", ((GET) annotation).value(), false); } else if (annotation instanceof HEAD) { parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false); } else if (annotation instanceof PATCH) { parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true); } else if (annotation instanceof POST) { parseHttpMethodAndPath("POST", ((POST) annotation).value(), true); } else if (annotation instanceof PUT) { parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
  31. HTTPϝιουΛ஍ಓʹύʔε if (annotation instanceof DELETE) { parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);

    } else if (annotation instanceof GET) { parseHttpMethodAndPath("GET", ((GET) annotation).value(), false); } else if (annotation instanceof HEAD) { parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false); } else if (annotation instanceof PATCH) { parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true); } else if (annotation instanceof POST) { parseHttpMethodAndPath("POST", ((POST) annotation).value(), true); } else if (annotation instanceof PUT) { parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
  32. HTTPϝιου͕ࢦఆ͞Εͯͳ͚Ε͹ɺ
 IllegalArgumentException͕εϩʔ͞Ε·͢ if (httpMethod == null) { throw methodError(method, 


    "HTTP method annotation is required (e.g., @GET, @POST, etc.)."); }
  33. body͕ແͯ͘(PATCH,POST,PUTҎ֎) @Multipart΍@FormUrlEncodedΛࢦఆͨ͠৔߹ɺ
 IllegalArgumentException͕εϩʔ͞Ε·͢ if (!hasBody) { if (isMultipart) { throw

    methodError(method, "Multipart can only be specified on HTTP methods with request body (e.g., @POST)."); } if (isFormEncoded) { throw methodError(method, "FormUrlEncoded can only be specified on HTTP methods with " + "request body (e.g., @POST)."); } }
  34. int start = converterFactories.indexOf(skipPast) + 1; for (int i =

    start, count = converterFactories.size(); i < count; i++) { Converter<ResponseBody, ?> converter = converterFactories.get(i) .responseBodyConverter(type, annotations, this); if (converter != null) { return (Converter<ResponseBody, T>) converter; } } StringBuilder builder = new StringBuilder("Could not locate ResponseBody converter for ") .append(type) .append(“.\n"); // লུ throw new IllegalArgumentException(builder.toString());
  35. int start = converterFactories.indexOf(skipPast) + 1; for (int i =

    start, count = converterFactories.size(); i < count; i++) { Converter<ResponseBody, ?> converter = converterFactories.get(i) .responseBodyConverter(type, annotations, this); if (converter != null) { return (Converter<ResponseBody, T>) converter; } } StringBuilder builder = new StringBuilder("Could not locate ResponseBody converter for ") .append(type) .append(“.\n"); // লུ throw new IllegalArgumentException(builder.toString());
  36. Converter<ResponseBody, ?> converter = converterFactories.get(i) .responseBodyConverter(type, annotations, this); if (converter

    != null) { return (Converter<ResponseBody, T>) converter; }
  37. Converter<ResponseBody, ?> converter = converterFactories.get(i) .responseBodyConverter(type, annotations, this); if (converter

    != null) { return (Converter<ResponseBody, T>) converter; }
  38. Converter<ResponseBody, ?> converter = converterFactories.get(i) .responseBodyConverter(type, annotations, this); if (converter

    != null) { return (Converter<ResponseBody, T>) converter; }
  39. Converter<ResponseBody, ?> converter = converterFactories.get(i) .responseBodyConverter(type, annotations, this); if (converter

    != null) { return (Converter<ResponseBody, T>) converter; }
  40. Converter<ResponseBody, ?> converter = converterFactories.get(i) .responseBodyConverter(type, annotations, this); if (converter

    != null) { return (Converter<ResponseBody, T>) converter; } responseBodyConverterϝιου͕ nullΛฦ͞ͳ͍Α͏ͳɺConverterFactory͕ converterFactoriesʹηοτͰ͖Ε͹ྑ͍ ͜ͱ͕Θ͔ͬͨɻ
  41. class MyConverterFactory : Converter.Factory() { override fun responseBodyConverter( type: Type,

    annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *>? { return MyResponseBodyConverter() } class MyResponseBodyConverter : Converter<ResponseBody, String> { override fun convert(value: ResponseBody): String? { return "test" } } }
  42. class MyConverterFactory : Converter.Factory() { override fun responseBodyConverter( type: Type,

    annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *>? { return MyResponseBodyConverter() } class MyResponseBodyConverter : Converter<ResponseBody, String> { override fun convert(value: ResponseBody): String? { return "test" } } }
  43. class MyConverterFactory : Converter.Factory() { override fun responseBodyConverter( type: Type,

    annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *>? { return MyResponseBodyConverter() } class MyResponseBodyConverter : Converter<ResponseBody, String> { override fun convert(value: ResponseBody): String? { return "test" } } }
  44. class MyConverterFactory : Converter.Factory() { override fun responseBodyConverter( type: Type,

    annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *>? { return MyResponseBodyConverter() } class MyResponseBodyConverter : Converter<ResponseBody, String> { override fun convert(value: ResponseBody): String? { return "test" } } }
  45. class MyConverterFactory : Converter.Factory() { override fun responseBodyConverter( type: Type,

    annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *>? { return MyResponseBodyConverter() } class MyResponseBodyConverter : Converter<ResponseBody, String> { override fun convert(value: ResponseBody): String? { return "test" } } }
  46. class MyConverterFactory : Converter.Factory() { override fun responseBodyConverter( type: Type,

    annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *>? { return MyResponseBodyConverter() } class MyResponseBodyConverter : Converter<ResponseBody, String> { override fun convert(value: ResponseBody): String? { return "test" } } }
  47. class MyConverterFactory : Converter.Factory() { override fun responseBodyConverter( type: Type,

    annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *>? { return MyResponseBodyConverter() } class MyResponseBodyConverter : Converter<ResponseBody, String> { override fun convert(value: ResponseBody): String? { return "test" } } }
  48. val retrofit = Retrofit.Builder() .baseUrl("https://api.github.com/") .addConverterFactory(MyConverterFactory()) .build()

  49. repos.enqueue(object : retrofit2.Callback<List<Repo>> { override fun onFailure(call: Call<List<Repo>>, t: Throwable)

    { Log.d("GitHubSample", t.message) } override fun onResponse( call: Call<List<Repo>>, response: Response<List<Repo>>) { Log.d("GitHubSample", response.toString()) } })
  50. repos.enqueue(object : retrofit2.Callback<List<Repo>> { override fun onFailure(call: Call<List<Repo>>, t: Throwable)

    { Log.d("GitHubSample", t.message) } override fun onResponse( call: Call<List<Repo>>, response: Response<List<Repo>>) { Log.d("GitHubSample", response.toString()) } }) Ϋϥογϡ͸͠ͳ͘ͳ͕ͬͨɺ ΄͍͠ͷ͸List<Repo>Ͱ StringͰ͸ͳ͍
  51. Retrofit͕ConverterΛ༻ҙ

  52. ConverterͰ ResponseBody ͔Β List<Repo>ʹม׵ https://github.com/Kotlin/kotlinx.serialization

  53. data class Repo(val name: String) ConverterͰ ResponseBody ͔Β List<Repo>ʹม׵

  54. import kotlinx.serialization.Serializable @Serializable data class Repo(val name: String) ConverterͰ ResponseBody

    ͔Β List<Repo>ʹม׵
  55. class MyResponseBodyConverter : Converter<ResponseBody, String> { override fun convert(value: ResponseBody):

    String? { return "test" } } ConverterͰ ResponseBody ͔Β List<Repo>ʹม׵
  56. class MyResponseBodyConverter : Converter<ResponseBody, List<Repo>> { override fun convert(value: ResponseBody):

    List<Repo>? { // ResponseBody͔ΒString΁ͷม׵෦෼͸লུ val json = Json { strictMode = false } return json.parse(Repo.serializer().list, content)) } } class MyResponseBodyConverter : Converter<ResponseBody, String> { override fun convert(value: ResponseBody): String? { return "test" } } ConverterͰ ResponseBody ͔Β List<Repo>ʹม׵
  57. List<Repo>Λऔಘ ConverterͰ ResponseBody ͔Β List<Repo>ʹม׵

  58. class MyConverterFactory : Converter.Factory() { override fun responseBodyConverter( type: Type,

    annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *>? { return MyResponseBodyConverter() } }
  59. class MyConverterFactory : Converter.Factory() { override fun responseBodyConverter( type: Type,

    annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *>? { return MyResponseBodyConverter() } override fun requestBodyConverter( type: Type, parameterAnnotations: Array<Annotation>, methodAnnotations: Array<Annotation>, retrofit: Retrofit ): Converter<*, RequestBody>? { return MyRequestBodyConverter() } }
  60. • Retrofit͸ΞϊςʔγϣϯΛ࢖ͬͯHTTPϝιου΍ύε Λࢦఆ͢Δ • Retrofit͸Proxy.newProxyInstanceΛ࢖ͬͯΞϊςʔ γϣϯΛύʔε͍ͯ͠Δ • RepoΫϥεͷΑ͏ͳಠࣗͷܕʹม׵͢Δʹ͸ɺ Converter͕ඞཁ ·ͱΊ

  61. ࠷ޙʹ(1) https://github.com/kwmt/RetrofitSample

  62. ࠷ޙʹ(2) https://github.com/JakeWharton/retrofit2-kotlinx-serialization-converter

  63. ࠷ޙʹ(3) DroidKaigi2020ͷCfSΛ ʮৄࡉRetrofit2ʯͱ͍͏λΠτϧͰग़͠·ͨ͠ʂ

  64. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ @kwmt27