├── .gitignore ├── LICENSE ├── build.gradle ├── docs ├── .nojekyll ├── README.md ├── _navbar.md ├── _sidebar.md ├── adapt.md ├── annotations.md ├── baseurl.md ├── cache.md ├── cookie.md ├── en │ └── README.md ├── getstart.md ├── headers.md ├── index.html ├── java │ ├── baseurl.md │ ├── cache.md │ ├── cookie.md │ ├── getstart.md │ ├── headers.md │ ├── json.md │ ├── progress.md │ ├── request.md │ └── response.md ├── json.md ├── progress.md ├── q&a.md ├── request.md └── response.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── retrofit-coroutines ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── dylanc │ └── retrofit │ └── coroutines │ ├── Dialogs.kt │ ├── DownloadApi.kt │ ├── Flow.kt │ ├── RequestViewModel.kt │ ├── RequestViewModelLazy.kt │ ├── exception │ └── RequestExceptionObserver.kt │ └── loading │ ├── RequestDialogFactory.kt │ ├── RequestDialogFragmentFactory.kt │ └── RequestLoadingObserver.kt ├── retrofit-ktx ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── dylanc │ │ └── retrofit │ │ ├── Retrofit.kt │ │ ├── RetrofitCache.kt │ │ ├── body │ │ ├── MultipartBody.kt │ │ ├── RequestBody.kt │ │ ├── RequestMap.kt │ │ └── ResponseBody.kt │ │ ├── cookiejar │ │ └── PersistentCookieJar.kt │ │ ├── helper │ │ └── RetrofitHelper.kt │ │ └── interceptor │ │ ├── CacheControlInterceptor.kt │ │ ├── DomainsInterceptor.kt │ │ ├── GlobalHeadersInterceptor.kt │ │ ├── HeadersInterceptor.kt │ │ ├── HttpLoggingInterceptor.kt │ │ └── ResponseBodyInterceptor.kt │ └── res │ ├── values │ └── strings.xml │ └── xml │ └── network_security_config.xml ├── retrofit-rxjava2 ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── dylanc │ └── retrofit │ └── rxjava2 │ ├── FileTransformer.kt │ ├── LoadingTransformer.kt │ ├── RequestLoading.kt │ ├── RxDownloadApi.kt │ ├── ThreadTransformer.kt │ └── Transformers.kt ├── retrofit-rxjava3 ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── dylanc │ │ └── retrofit │ │ └── rxjava3 │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── dylanc │ │ └── retrofit │ │ └── rxjava3 │ │ ├── FileTransformer.kt │ │ ├── LoadingTransformer.kt │ │ ├── RequestLoading.kt │ │ ├── RxDownloadApi.kt │ │ ├── ThreadTransformer.kt │ │ └── Transformers.kt │ └── test │ └── java │ └── com │ └── dylanc │ └── retrofit │ └── rxjava3 │ └── ExampleUnitTest.kt ├── sample-java ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── dylanc │ │ └── retrofit │ │ └── sample │ │ └── java │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── dylanc │ │ │ └── retrofit │ │ │ └── sample │ │ │ └── java │ │ │ ├── App.java │ │ │ ├── api │ │ │ ├── GankApi.kt │ │ │ └── RxJavaApi.kt │ │ │ ├── bean │ │ │ ├── ApiResponse.kt │ │ │ └── UserBean.kt │ │ │ ├── constant │ │ │ └── Constants.kt │ │ │ ├── network │ │ │ ├── ApiException.kt │ │ │ ├── AutoDisposable.java │ │ │ ├── DebugInterceptor.kt │ │ │ ├── GlobalErrorHandler.kt │ │ │ ├── LoadingDialog.kt │ │ │ ├── ProgressTransformer.kt │ │ │ └── SslSocketUtils.java │ │ │ └── ui │ │ │ └── JavaActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_sample.xml │ │ └── dialog_loading.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── raw │ │ ├── login_failure.json │ │ └── login_success.json │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── dylanc │ └── retrofit │ └── sample │ └── java │ └── ExampleUnitTest.kt ├── sample-kotlin ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── dylanc │ │ └── retrofit │ │ └── sample │ │ └── kotlin │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── dylanc │ │ │ └── retrofit │ │ │ └── sample │ │ │ └── kotlin │ │ │ ├── App.kt │ │ │ ├── api │ │ │ ├── CoroutinesApi.kt │ │ │ ├── GankApi.kt │ │ │ └── RxJavaApi.kt │ │ │ ├── bean │ │ │ ├── ApiResponse.kt │ │ │ └── UserBean.kt │ │ │ ├── constant │ │ │ └── Constants.kt │ │ │ ├── network │ │ │ ├── LoadingDialogFactory.kt │ │ │ └── LoadingDialogFragmentFactory.kt │ │ │ ├── repository │ │ │ └── DataRepository.kt │ │ │ ├── ui │ │ │ ├── MainActivity.kt │ │ │ ├── mvp │ │ │ │ ├── RxJavaSampleActivity.kt │ │ │ │ ├── RxJavaSampleContract.kt │ │ │ │ ├── RxJavaSampleModel.kt │ │ │ │ └── RxJavaSamplePresenter.kt │ │ │ └── mvvm │ │ │ │ ├── CoroutinesSampleActivity.kt │ │ │ │ └── CoroutinesSampleViewModel.kt │ │ │ └── widget │ │ │ └── LoadingDialogFragment.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── activity_sample.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── dylanc │ └── retrofit │ └── sample │ └── kotlin │ └── ExampleUnitTest.kt └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | autodispose_version = '1.4.0' 4 | coroutines_version = '1.5.0' 5 | kotlin_version = '1.6.0' 6 | lifecycle_extensions_version = '2.2.0' 7 | lifecycle_version = '2.3.1' 8 | okhttp_version = '4.9.0' 9 | retrofit_version = '2.9.0' 10 | rxandroid_version = '2.1.1' 11 | retrofit_helper_annotations_version = '2.0.0' 12 | } 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | dependencies { 18 | classpath 'com.android.tools.build:gradle:4.2.1' 19 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 20 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' 21 | } 22 | } 23 | 24 | allprojects { 25 | repositories { 26 | google() 27 | jcenter() 28 | maven { url 'https://www.jitpack.io' } 29 | } 30 | } 31 | 32 | task clean(type: Delete) { 33 | delete rootProject.buildDir 34 | } -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DylanCaiCoding/RetrofitKTX/266af7b26da9abdb0508c6f5a4d4db93645df582/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # RetrofitKTX 2 | 3 | [![](https://jitpack.io/v/DylanCaiCoding/RetrofitKTX.svg)](https://jitpack.io/#DylanCaiCoding/RetrofitKTX) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://github.com/DylanCaiCoding/RetrofitHelper/blob/master/LICENSE) 4 | 5 | ## Gradle 6 | 7 | 在根目录的 build.gradle 添加: 8 | 9 | ```groovy 10 | allprojects { 11 | repositories { 12 | // ... 13 | maven { url 'https://www.jitpack.io' } 14 | } 15 | } 16 | ``` 17 | 18 | 添加依赖: 19 | 20 | ```groovy 21 | dependencies { 22 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-ktx:1.0.0-beta' 23 | kapt 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-compiler:1.0.0-beta' 24 | 25 | // 可选 26 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-coroutines:1.0.0-beta' 27 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-rxjava2:1.0.0-beta' 28 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-rxjava3:1.0.0-beta' 29 | } 30 | ``` 31 | 32 | ## License 33 | 34 | ``` 35 | Copyright (C) 2019. Dylan Cai 36 | 37 | Licensed under the Apache License, Version 2.0 (the "License"); 38 | you may not use this file except in compliance with the License. 39 | You may obtain a copy of the License at 40 | 41 | http://www.apache.org/licenses/LICENSE-2.0 42 | 43 | Unless required by applicable law or agreed to in writing, software 44 | distributed under the License is distributed on an "AS IS" BASIS, 45 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 46 | See the License for the specific language governing permissions and 47 | limitations under the License. 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | * [En](/en/) 2 | * [中文](/) -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [首页](/) 2 | 3 | * Kotlin 用法 4 | * [开始上手](/getstart) 5 | * [管理 base url](/baseurl) 6 | * [管理 headers](/headers) 7 | * [发起请求](/request?id=发起请求) 8 | * [使用协程回调结果](/request?id=协程回调) 9 | * [使用 RxJava 回调结果](/request?id=rxjava-回调) 10 | * [缓存策略](/cache) 11 | * [提交 Json 数据](/json) 12 | * [持久化 cookie](/cookie) 13 | * [处理后台的骚数据](/response) 14 | * [监听上传或下载进度](/progress) 15 | 16 | * Java 用法 17 | * [开始上手](/java/getstart) 18 | * [管理 base url](/java/baseurl) 19 | * [管理 headers](/java/headers) 20 | * [发起请求](/java/request) 21 | * [缓存策略](/java/cache) 22 | * [提交 Json 数据](/java/json) 23 | * [持久化 cookie](/java/cookie) 24 | * [处理后台的骚数据](/java/response) 25 | * [监听上传或下载进度](/java/progress) 26 | 27 | * 其它 28 | * [适配已有项目](/adapt) 29 | * [注解的含义](annotations.md) 30 | * [Q&A](q&a) -------------------------------------------------------------------------------- /docs/adapt.md: -------------------------------------------------------------------------------- 1 | # 适配已有项目 -------------------------------------------------------------------------------- /docs/annotations.md: -------------------------------------------------------------------------------- 1 | # 注解的含义 2 | -------------------------------------------------------------------------------- /docs/baseurl.md: -------------------------------------------------------------------------------- 1 | # 管理 base URL 2 | 3 | ### 默认域名 4 | 5 | 用注解 `@BaseUrl` 配置默认域名,例如: 6 | 7 | ```kotlin 8 | @BaseUrl 9 | const val BASE_URL = "https://www.wanandroid.com/" 10 | ``` 11 | 12 | ### 测试域名 13 | 14 | 用注解 `@DebugUrl` 配置测试域名,并在初始化时配置了 debug 才会生效,否则使用默认域名。例如: 15 | 16 | ```kotlin 17 | @DebugUrl 18 | const val DEBUG_URL = "http://192.168.1.3" 19 | ``` 20 | 21 | ```kotlin 22 | initRetrofit(BuildConfig.DEBUG) { 23 | // ... 24 | } 25 | ``` 26 | 27 | ### 静态多域名 28 | 29 | 在接口类增加 `@ApiUrl` 注解来修改该类所有请求方法的 baseUrl,例如: 30 | 31 | ```kotlin 32 | @ApiUrl("https://gank.io") 33 | interface GankApi{ 34 | @GET("/api/today") 35 | fun getTodayList(): Single 36 | } 37 | ``` 38 | 39 | > 使用 `@ApiUrl` 注解会增加 Retrofit 实例,如果对此十分介意可选择动态域名的用法。 40 | 41 | ### 动态域名 42 | 43 | 在请求的方法增加 `DomainName`注解,为该请求标注一个域名代号,例如: 44 | 45 | ```kotlin 46 | @DomainName("wanandroid") 47 | @GET("/article/list/{page}/json") 48 | fun geArticleList(@Path(value = "page") page: Int): Single 49 | ``` 50 | 51 | 在一个地址常量增加 `@DomainUrl` 注解并传入上面的域名代号,可修改该域名代号的默认地址,例如: 52 | 53 | ```kotlin 54 | @DomainUrl("wanandroid") 55 | const val WAN_ANDROID_URL = "https://www.wanandroid.com" 56 | ``` 57 | 58 | 在运行时想动态修改域名,只需修改 retrofitDomains 中对应代号的值即可。例如: 59 | 60 | ```kotlin 61 | retrofitDomains["wanandroid"] = "https://www.wanandroid.com/v2" 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/cache.md: -------------------------------------------------------------------------------- 1 | # 缓存策略 -------------------------------------------------------------------------------- /docs/cookie.md: -------------------------------------------------------------------------------- 1 | # 持久化 cookie 2 | 3 | 初始化时配置持久化 cookie,例如: 4 | 5 | ```kotlin 6 | initRetrofit { 7 | persistentCookies() 8 | } 9 | ``` 10 | 11 | 如果退出的时候想清理 cookie ,可以调用清理方法,例如: 12 | 13 | ```kotlin 14 | clearPersistentCookies() 15 | ``` -------------------------------------------------------------------------------- /docs/en/README.md: -------------------------------------------------------------------------------- 1 | # RetrofitKTX 2 | 3 | [![](https://jitpack.io/v/DylanCaiCoding/RetrofitKTX.svg)](https://jitpack.io/#DylanCaiCoding/RetrofitKTX) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://github.com/DylanCaiCoding/RetrofitHelper/blob/master/LICENSE) 4 | 5 | ## Gradle 6 | 7 | Add it in your root build.gradle at the end of repositories: 8 | 9 | ```groovy 10 | allprojects { 11 | repositories { 12 | // ... 13 | maven { url 'https://www.jitpack.io' } 14 | } 15 | } 16 | ``` 17 | 18 | Add dependencies: 19 | 20 | ```groovy 21 | dependencies { 22 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-ktx:1.0.0-alpha' 23 | kapt 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-compiler:1.0.0-alpha' 24 | 25 | // The following are optional, please add as needed 26 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-coroutines:1.0.0-alpha' 27 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-rxjava2:1.0.0-alpha' 28 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-autodispose:1.0.0-alpha' 29 | } 30 | ``` 31 | 32 | ## License 33 | 34 | ``` 35 | Copyright (C) 2019. Dylan Cai 36 | 37 | Licensed under the Apache License, Version 2.0 (the "License"); 38 | you may not use this file except in compliance with the License. 39 | You may obtain a copy of the License at 40 | 41 | http://www.apache.org/licenses/LICENSE-2.0 42 | 43 | Unless required by applicable law or agreed to in writing, software 44 | distributed under the License is distributed on an "AS IS" BASIS, 45 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 46 | See the License for the specific language governing permissions and 47 | limitations under the License. 48 | ``` 49 | 50 | -------------------------------------------------------------------------------- /docs/getstart.md: -------------------------------------------------------------------------------- 1 | # 开始上手 2 | 3 | ## 添加依赖 4 | 5 | 在根目录的 build.gradle 添加: 6 | 7 | ```groovy 8 | allprojects { 9 | repositories { 10 | // ... 11 | maven { url 'https://www.jitpack.io' } 12 | } 13 | } 14 | ``` 15 | 16 | 添加依赖: 17 | 18 | ```groovy 19 | dependencies { 20 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-ktx:1.0.0-beta' 21 | kapt 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-compiler:1.0.0-beta' 22 | 23 | // 可选 24 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-coroutines:1.0.0-beta' 25 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-rxjava2:1.0.0-beta' 26 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-rxjava3:1.0.0-beta' 27 | } 28 | ``` 29 | 30 | ## 初始化 31 | 32 | 初始化是非必要的,以下是可选的常用配置: 33 | 34 | ```kotlin 35 | initRetrofit(BuildConfig.DEBUG) { 36 | okHttpClient { 37 | addHeaders("name1" to "value1", "name2" to "value2") // 全局 headers 38 | printHttpLog { Log.i("http", it) } // 打印日志 39 | cacheControl { 40 | if (!isNetworkAvailable) { 41 | maxAge(1, TimeUnit.DAYS) // 比如在网络不可用时取一天内的缓存 42 | } 43 | } 44 | multipleDomains() // 支持动态域名 45 | persistentCookies() // 持久化 cookies 46 | connectTimeout(15, TimeUnit.SECONDS) 47 | writeTimeout(15, TimeUnit.SECONDS) 48 | readTimeout(15, TimeUnit.SECONDS) 49 | retryOnConnectionFailure(false) 50 | authenticator(authenticator) 51 | addInterceptor(interceptor) 52 | } 53 | addConverterFactory(ScalarsConverterFactory.create()) 54 | addCallAdapterFactory(RxJava3CallAdapterFactory.create()) 55 | } 56 | ``` -------------------------------------------------------------------------------- /docs/headers.md: -------------------------------------------------------------------------------- 1 | # 管理 headers 2 | 3 | ## 添加全局 header 4 | 5 | 初始化时调用 `addHeaders()` 配置全局 headers。 6 | 7 | ```kotlin 8 | initRetrofit { 9 | okHttpClient { 10 | addHeaders("name1" to "value1", "name2" to "value2") 11 | } 12 | } 13 | ``` 14 | 15 | ## 添加静态 header 16 | 17 | 在接口方法添加 `@Headers` 注解进行修饰。 18 | 19 | ```kotlin 20 | interface UserApi { 21 | @Headers("Content-Type: application/json;charset=UTF-8") 22 | @GET("/user") 23 | suspend fun geUser(): User 24 | } 25 | ``` 26 | 27 | ## 动态添加 header 28 | 29 | 在接口方法添加 `@Header` 注解修饰的参数,在发起请求时才传入 header。 30 | 31 | ```kotlin 32 | interface UserApi { 33 | @GET("/user") 34 | suspend fun geUser(@Header("Country-Code") countryCode: String): User 35 | } 36 | ``` 37 | 38 | 如果有很多接口方法需要动态添加相同的 header,建议使用下面的方式进行配置,支持对 url 进行判断。 39 | 40 | ```kotlin 41 | initRetrofit { 42 | // ... 43 | addHeaders { request -> 44 | if (request.url.toString().startsWith("...")) { 45 | put("Country-Code", countryCode) 46 | } 47 | } 48 | } 49 | ``` -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/java/baseurl.md: -------------------------------------------------------------------------------- 1 | # 管理 base URL 2 | 3 | ## 默认域名 4 | 5 | 用注解 `@BaseUrl` 配置默认域名,例如: 6 | 7 | ```java 8 | public class Constants { 9 | @BaseUrl 10 | public static final String BASE_URL = "https://www.wanandroid.com"; 11 | } 12 | ``` 13 | 14 | ## 测试域名 15 | 16 | 用注解 `@DebugUrl` 配置测试域名,并在初始化时配置了 debug 才会生效,不然会使用默认地址。例如: 17 | 18 | ```java 19 | public class Constants { 20 | @DebugUrl 21 | public static final String DEBUG_URL = "http://192.168.1.3"; 22 | } 23 | ``` 24 | 25 | ```java 26 | RetrofitHelper.getDefault() 27 | .debug(BuildConfig.DEBUG) 28 | ... 29 | .init(); 30 | ``` 31 | 32 | ## 多域名 33 | 34 | 在接口类增加 `@ApiUrl` 注解来修改该类请求的 baseUrl,例如: 35 | 36 | ```java 37 | @ApiUrl("https://gank.io") 38 | public interface GankApi{ 39 | @GET("/api/today") 40 | Single getTodayList(); 41 | } 42 | ``` 43 | 44 | ## 动态域名 45 | 46 | 在运行时请求的 baseUrl 会变,这个需求相对较少,通常只是不同域名,不需要运行时改变。所以本库将该功能移除了,但是代码仍有保留,有需要可以拷贝 [DomainsInterceptor](https://github.com/DylanCaiCoding/RetrofitHelper/blob/master/app/src/main/java/com/dylanc/retrofit/helper/sample/network/DomainsInterceptor.kt) 文件来使用。 47 | 48 | 接下来讲下用法,创建一个 map 来管理动态的地址,用代号+地址的方式保存,例如: 49 | 50 | ```java 51 | public class Constants { 52 | public static HashMap domains = new HashMap<>(); 53 | 54 | static { 55 | domains.put("wanandroid", "https://www.wanandroid.com"); 56 | } 57 | } 58 | ``` 59 | 60 | 初始化时添加 DomainsInterceptor,传入该 map,例如: 61 | 62 | ```java 63 | RetrofitHelper.getDefault() 64 | ... 65 | .addInterceptor(new DomainsInterceptor(Constants.domains)) 66 | .init(); 67 | ``` 68 | 69 | 在请求的方法增加一个 `DOMAIN_HEADER` + 代号的 header,例如: 70 | 71 | ```java 72 | @Headers(Domain.DOMAIN_HEADER + "wanandroid") 73 | @GET("/article/list/{page}/json") 74 | Single geArticleList(@Path(value = "page") int page); 75 | ``` 76 | 77 | 上述的完成后,该请求的 baseUrl 就会使用代号所对应的地址,那么我们只需修改 map 的值,即可做到动态域名。例如: 78 | 79 | ```java 80 | Constants.domains.put("wanandroid", "https://www.wanandroid2.com"); 81 | ``` 82 | -------------------------------------------------------------------------------- /docs/java/cache.md: -------------------------------------------------------------------------------- 1 | # 缓存策略 -------------------------------------------------------------------------------- /docs/java/cookie.md: -------------------------------------------------------------------------------- 1 | # 持久化 cookie 2 | 3 | 初始化时配置持久化 cookie,例如: 4 | 5 | ```java 6 | RetrofitHelper.getDefault() 7 | .cookieJar(PersistentCookies.create(this)) 8 | .init(); 9 | ``` 10 | 11 | 如果退出的时候想清理 cookie ,可以调用清理方法,例如: 12 | 13 | ```java 14 | PersistentCookies.clear() 15 | ``` -------------------------------------------------------------------------------- /docs/java/getstart.md: -------------------------------------------------------------------------------- 1 | # 开始上手 2 | 3 | ## 添加依赖 4 | 5 | 在根目录的 build.gradle 添加: 6 | 7 | ```groovy 8 | allprojects { 9 | repositories { 10 | // ... 11 | maven { url 'https://www.jitpack.io' } 12 | } 13 | } 14 | ``` 15 | 16 | 添加依赖: 17 | 18 | ```groovy 19 | dependencies { 20 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-ktx:1.0.0-beta' 21 | kapt 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-compiler:1.0.0-beta' 22 | 23 | // 可选 24 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-coroutines:1.0.0-beta' 25 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-rxjava2:1.0.0-beta' 26 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-rxjava3:1.0.0-beta' 27 | } 28 | ``` 29 | 30 | ## 初始化 31 | 32 | 初始化是非必要的,以下是可选的常用配置: 33 | 34 | ```java 35 | RetrofitHelper.getDefault() 36 | .addHeader("key", "value") 37 | .cache(new File(getCacheDir(), "response"), 10 * 1024 * 1024, 38 | () -> new CacheControl.Builder().maxAge(10, TimeUnit.MINUTES).build()) 39 | .connectTimeout(15) 40 | .writeTimeout(15) 41 | .readTimeout(15) 42 | .retryOnConnectionFailure(false) 43 | .authenticator(authenticator) 44 | .cookieJar(cookieJar) 45 | .addInterceptor(interceptor) 46 | .addNetworkInterceptor(networkInterceptor) 47 | .addConverterFactory(FastJsonConverterFactory.create()) 48 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 49 | .init(); 50 | ``` 51 | 52 | 如果上述提供的常用配置方法还不满足需求,可以配置 OkHttpClientBuilder 和 RetrofitBuilder 达到所需的效果,比如: 53 | 54 | ```java 55 | RetrofitHelper.getDefault() 56 | // 其它配置 57 | .okHttpClientBuilder(builder -> { 58 | builder.sslSocketFactory(sslSocketFactory); 59 | return Unit.INSTANCE; 60 | }) 61 | .retrofitBuilder(builder -> { 62 | builder.validateEagerly(false); 63 | return Unit.INSTANCE; 64 | }) 65 | .init(); 66 | ``` -------------------------------------------------------------------------------- /docs/java/headers.md: -------------------------------------------------------------------------------- 1 | # 管理 headers 2 | -------------------------------------------------------------------------------- /docs/java/json.md: -------------------------------------------------------------------------------- 1 | # 提交 Json 数据 2 | 3 | Retrofit 提交 Json 数据需要把请求方法的参数改成 RequestBody 并加上 `@Body` 注解,例如: 4 | 5 | ```java 6 | @Post("/user/login") 7 | Single> login(@Body RequestBody requestBody); 8 | ``` 9 | 10 | 如果是想直接用键值对请求,可以用 RequestBodyFactory 创建 RequestBody 对象,例如: 11 | 12 | ```java 13 | Map params = new HashMap<>(); 14 | params.put("username", username); 15 | params.put("password", password); 16 | api.login(RequestBodyFactory.create(params)) 17 | ... 18 | ``` 19 | 20 | 如果已有请求的 Json 的实体类,例如: 21 | 22 | ```java 23 | public class UserForm { 24 | private String username; 25 | private String pwd; 26 | 27 | public UserForm(String username, String pwd) { 28 | this.username = username; 29 | this.pwd = pwd; 30 | } 31 | 32 | public String getUsername() { 33 | return username; 34 | } 35 | 36 | public void setUsername(String username) { 37 | this.username = username; 38 | } 39 | 40 | public String getPwd() { 41 | return pwd; 42 | } 43 | 44 | public void setPwd(String pwd) { 45 | this.pwd = pwd; 46 | } 47 | } 48 | ``` 49 | 50 | 那么可以将该类对象用 RequestBodyFactory 转成 RequestBody 对象,例如: 51 | 52 | ```java 53 | UserForm userForm = new UserForm(username, pwd); 54 | api.login(RequestBodyFactory.create(userForm)) 55 | ... 56 | ``` -------------------------------------------------------------------------------- /docs/java/progress.md: -------------------------------------------------------------------------------- 1 | # 监听上传或下载进度 -------------------------------------------------------------------------------- /docs/java/request.md: -------------------------------------------------------------------------------- 1 | # 发起请求 2 | 3 | ## 准备工作 4 | 5 | 添加相应的 RxJava 依赖: 6 | 7 | ``` 8 | dependencies { 9 | implementation 'com.dylanc:retrofit-helper-rxjava:1.2.0' 10 | } 11 | ``` 12 | 13 | 初始化时配置 `RxJava2CallAdapterFactory`: 14 | 15 | ```java 16 | RetrofitHelper.getDefault() 17 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 18 | .init(); 19 | ``` 20 | 21 | 如果要在请求的时候显示加载弹框,先创建一个类实现 RequestLoading 的接口,重写 show 法。例如: 22 | ```java 23 | class LoadingDialog extends DialogFragment implements RequestLoading { 24 | 25 | private FragmentActivity fragmentActivity; 26 | 27 | public LoadingDialog(FragmentActivity fragmentActivity) { 28 | this.fragmentActivity = fragmentActivity; 29 | } 30 | 31 | @NonNull 32 | @Override 33 | public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { 34 | return new AlertDialog.Builder(fragmentActivity) 35 | .setTitle("loading") 36 | .setMessage("wait a minute...") 37 | .setCancelable(false) 38 | .create(); 39 | } 40 | 41 | @Override 42 | public void show(boolean isLoading) { 43 | if (isLoading) { 44 | show(fragmentActivity.getSupportFragmentManager(), "loading"); 45 | } else { 46 | dismiss(); 47 | } 48 | } 49 | } 50 | 51 | ``` 52 | 53 | ## Get 请求 54 | 55 | ```java 56 | public interface ArticleApi { 57 | @GET("/article/list/{page}/json") 58 | Single geArticleList(@Path(value = "page") int page); 59 | } 60 | ``` 61 | 62 | ```java 63 | RetrofitHelper.create(ArticleApi.class) 64 | .geArticleList(page) 65 | .compose(Transformers.io2mainThread()) // 切换线程 66 | .compose(Transformers.showLoading(new LoadingDialog(this))) // 显示弹框,参数是 RequestLoading 接口 67 | .as(AutoDisposable.bind(this)) // 绑定生命周期 68 | .subscribe( 69 | json -> { 70 | Toast.makeText(this, json, Toast.LENGTH_SHORT).show() 71 | }, 72 | e -> { 73 | Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show(); 74 | } 75 | ); 76 | ``` 77 | 78 | ## Post 请求 79 | 80 | ```java 81 | public interface UserApi{ 82 | @FormUrlEncoded 83 | @POST("/user/login") 84 | Single login(@Field("username") String username, @Field("password") String password); 85 | } 86 | ``` 87 | 88 | ```java 89 | RetrofitHelper.create(UserApi.class) 90 | .login(username, password) 91 | .compose(Transformers.io2mainThread()) 92 | .compose(Transformers.showLoading(new RxLoadingDialog(this))) 93 | .as(AutoDisposable.bind(this)) 94 | .subscribe( 95 | json -> { 96 | Toast.makeText(this, json, Toast.LENGTH_SHORT).show() 97 | }, 98 | e -> { 99 | Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show(); 100 | } 101 | ); 102 | ``` 103 | 104 | ## 上传文件 105 | 106 | ```java 107 | public interface UploadApi{ 108 | @Multipart 109 | @POST("/file/upload") 110 | Single uploadImage(@Part MultipartBody.Part image); // 单文件上传 111 | 112 | @Multipart 113 | @POST("/file/upload") 114 | Single uploadImages(@Part List images); // 多文件上传 115 | } 116 | ``` 117 | 118 | ```java 119 | RetrofitHelper.create(UploadApi.class) 120 | .uploadImage(PartFactory.create(path, "file")) 121 | //.uploadImages(PartFactory.create(pathList, "files")) 122 | .compose(Transformers.io2mainThread()) 123 | .compose(Transformers.showLoading(new RxLoadingDialog(this))) 124 | .as(AutoDisposable.bind(this)) 125 | .subscribe( 126 | json -> { 127 | Toast.makeText(this, "上传成功", Toast.LENGTH_SHORT).show() 128 | }, 129 | e -> { 130 | Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show(); 131 | } 132 | ); 133 | ``` 134 | 135 | ## 下载文件 136 | 137 | ```java 138 | public interface UploadApi{ 139 | @Streaming 140 | @GET 141 | Single download(@Url String url); 142 | } 143 | ``` 144 | 145 | ```java 146 | RetrofitHelper.create(DownloadApi.class) 147 | .download(url) 148 | .compose(Transformers.toFile(pathname)) 149 | .compose(Transformers.io2mainThread()) 150 | .compose(Transformers.showLoading(new RxLoadingDialog(this))) 151 | .as(AutoDisposable.bind(this)) 152 | .subscribe( 153 | file -> { 154 | Toast.makeText(this, "已下载到" + file.getPath(), Toast.LENGTH_SHORT).show() 155 | }, 156 | e -> { 157 | Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show(); 158 | } 159 | ); 160 | ``` -------------------------------------------------------------------------------- /docs/java/response.md: -------------------------------------------------------------------------------- 1 | # 处理后台的骚数据 2 | 3 | 这是个人写的一篇文章[《优雅地处理后台返回的骚数据》](https://juejin.im/post/6844903975028785159)所提供的方案,或者说该文章是我封装此功能时有感而发。简单总结就是,后台返回的数据不方便我们解析,建议让后台人员改。如果实现没办法沟通后仍不肯改,就自己手动预处理数据,改成自己想要的结构,这样解析就不会报错。 4 | 5 | 在初始化时可以配置 doOnRepsonse 方法预处理数据,例如: 6 | 7 | ```java 8 | RetrofitHelper.getDefault() 9 | .doOnResponse((response, url, body) -> { 10 | ... 11 | }) 12 | .init(); 13 | ``` 14 | 15 | 具体的预处理代码怎么写呢,可以参考文章里的示例。 -------------------------------------------------------------------------------- /docs/json.md: -------------------------------------------------------------------------------- 1 | # 提交 Json 数据 2 | 3 | ### 使用键值对请求 4 | 5 | 将方法参数定义为 RequestMap,并用 `@Body` 注解修饰,例如: 6 | 7 | ```kotlin 8 | @Post("/user/login") 9 | fun login(@Body params: RequestMap): Single> 10 | ``` 11 | 12 | 发起请求,可用 `mapOf()` 方法创建键值对,例如: 13 | 14 | ```kotlin 15 | api.login(mapOf("username" to username, "pwd" to pwd) 16 | ... 17 | ``` 18 | 19 | ### 使用实体类请求 20 | 21 | 如果已有请求的 Json 的实体类,例如: 22 | 23 | ```kotlin 24 | data class UserForm(val username: String, val pwd: String) 25 | ``` 26 | 27 | 那么可以将该类对象作为接口方法参数,并用 `@Body` 修饰,例如: 28 | 29 | ```kotlin 30 | @Post("/user/login") 31 | fun login(@Body userForm: UserForm): Single> 32 | ``` 33 | 34 | 发起请求: 35 | 36 | ```kotlin 37 | val userForm = UserForm(username, pwd) 38 | api.login(userForm) 39 | ... 40 | ``` -------------------------------------------------------------------------------- /docs/progress.md: -------------------------------------------------------------------------------- 1 | # 监听上传或下载进度 -------------------------------------------------------------------------------- /docs/q&a.md: -------------------------------------------------------------------------------- 1 | # Q&A -------------------------------------------------------------------------------- /docs/request.md: -------------------------------------------------------------------------------- 1 | # 发起请求 2 | 3 | ## 协程回调 4 | 5 | ## RxJava 回调 6 | 7 | ### 准备工作 8 | 9 | 添加相应的 RxJava 依赖: 10 | 11 | ```groovy 12 | dependencies { 13 | // implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-rxjava2:1.0.0-beta' 14 | implementation 'com.github.DylanCaiCoding.RetrofitKTX:retrofit-rxjava3:1.0.0-beta' 15 | } 16 | ``` 17 | 18 | 初始化时配置 `RxJava2CallAdapterFactory`: 19 | 20 | ```kotlin 21 | initRetrofit { 22 | // addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 23 | addCallAdapterFactory(RxJava3CallAdapterFactory.create()) 24 | } 25 | ``` 26 | 27 | 如果要在请求的时候显示加载弹框,先创建一个类实现 RequestLoading 的接口,重写 show 法。例如: 28 | ```kotlin 29 | class LoadingDialog(private val fragmentActivity: FragmentActivity) : DialogFragment(), RequestLoading { 30 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 31 | return AlertDialog.Builder(fragmentActivity) 32 | .setTitle("loading") 33 | .setMessage("wait a minute...") 34 | .setCancelable(false) 35 | .create() 36 | } 37 | 38 | override fun show(isLoading: Boolean) { 39 | if (isLoading) { 40 | show(fragmentActivity.supportFragmentManager, "loading") 41 | } else { 42 | dismiss() 43 | } 44 | } 45 | } 46 | ``` 47 | 48 | ### Get 请求 49 | 50 | ```kotlin 51 | interface ArticleApi { 52 | @GET("/article/list/{page}/json") 53 | fun geArticleList(@Path(value = "page") page:Int): Single 54 | } 55 | ``` 56 | 57 | ```kotlin 58 | apiServiceOf() 59 | .geArticleList(page) 60 | .io2mainThread() // 切换线程 61 | .showLoading(LoadingDialog(this)) // 显示弹框,参数是 RequestLoading 接口 62 | .autoDispose(this) // 绑定生命周期 63 | .subscribe({ json -> 64 | Toast.makeText(this, json, Toast.LENGTH_SHORT).show() 65 | }, { e -> 66 | Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show() 67 | }) 68 | ``` 69 | 70 | ### Post 请求 71 | 72 | ```kotlin 73 | interface UserApi{ 74 | @FormUrlEncoded 75 | @POST("/user/login") 76 | fun login(@Field("username") username: String, @Field("password") password: String): Single 77 | } 78 | ``` 79 | 80 | ```kotlin 81 | apiServiceOf() 82 | .login(username, password) 83 | .io2mainThread() 84 | .showLoading(LoadingDialog(this)) 85 | .autoDispose(this) 86 | .subscribe({ json -> 87 | Toast.makeText(this, json, Toast.LENGTH_SHORT).show() 88 | }, { e -> 89 | Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show() 90 | }) 91 | ``` 92 | 93 | ### 上传文件 94 | 95 | ```kotlin 96 | interface UploadApi{ 97 | @Multipart 98 | @POST("/file/upload") 99 | fun uploadImage(@Part image: MultipartBody.Part): Single // 单文件上传 100 | 101 | @Multipart 102 | @POST("/file/upload") 103 | fun uploadImages(@Part images: List): Single // 多文件上传 104 | } 105 | ``` 106 | 107 | ```kotlin 108 | apiServiceOf() 109 | .uploadImage(path.toPart("file")) 110 | //.uploadImages(pathList.toPartList("files")) 111 | .io2mainThread() 112 | .showLoading(LoadingDialog(this)) 113 | .autoDispose(this) 114 | .subscribe({ 115 | Toast.makeText(this, "上传成功", Toast.LENGTH_SHORT).show() 116 | }, { e -> 117 | Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show() 118 | }) 119 | ``` 120 | 121 | ### 下载文件 122 | 123 | ```kotlin 124 | interface DownloadApi{ 125 | @Streaming 126 | @GET 127 | fun download(@Url url: String): Single 128 | } 129 | ``` 130 | 131 | ```kotlin 132 | apiServiceOf() 133 | .download(url) 134 | .toFile(pathname) 135 | .showLoading(LoadingDialog(this)) 136 | .autoDispose(this) 137 | .subscribe({ file -> 138 | Toast.makeText(this, "已下载到${file.path}", Toast.LENGTH_SHORT).show() 139 | }, { e -> 140 | Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show() 141 | }) 142 | ``` -------------------------------------------------------------------------------- /docs/response.md: -------------------------------------------------------------------------------- 1 | # 处理后台的骚数据 2 | 3 | 这是个人写的一篇文章[《优雅地处理后台返回的骚数据》](https://juejin.im/post/6844903975028785159)所提供的方案,或者说该文章是我封装此功能时有感而发。简单总结就是,后台返回的数据不方便我们解析,建议让后台人员改。如果实现没办法沟通后仍不肯改,就自己手动预处理数据,改成自己想要的结构,这样解析就不会报错。 4 | 5 | 在初始化时可以配置 doOnRepsonse 方法预处理数据,例如: 6 | 7 | ```kotlin 8 | initRetrofit { 9 | doOnResponse { response, url, body -> 10 | ... 11 | } 12 | } 13 | ``` 14 | 15 | 具体的预处理代码怎么写呢,可以参考文章里的示例。 -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DylanCaiCoding/RetrofitKTX/266af7b26da9abdb0508c6f5a4d4db93645df582/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jun 02 20:16:03 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /retrofit-coroutines/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /retrofit-coroutines/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'com.github.dcendents.android-maven' 5 | 6 | group = 'com.github.DylanCaiCoding' 7 | 8 | android { 9 | compileSdkVersion 30 10 | buildToolsVersion "29.0.3" 11 | 12 | defaultConfig { 13 | minSdkVersion 21 14 | consumerProguardFiles "consumer-rules.pro" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | compileOptions { 25 | kotlinOptions.freeCompilerArgs += ['-module-name', "retrofit_coroutines"] 26 | } 27 | } 28 | 29 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 30 | kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"] 31 | } 32 | 33 | dependencies { 34 | implementation fileTree(dir: "libs", include: ["*.jar"]) 35 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version" 36 | implementation 'androidx.fragment:fragment-ktx:1.3.6' 37 | implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0-alpha03' 38 | api 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-alpha03' 39 | api "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" 40 | } 41 | -------------------------------------------------------------------------------- /retrofit-coroutines/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DylanCaiCoding/RetrofitKTX/266af7b26da9abdb0508c6f5a4d4db93645df582/retrofit-coroutines/consumer-rules.pro -------------------------------------------------------------------------------- /retrofit-coroutines/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /retrofit-coroutines/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /retrofit-coroutines/src/main/java/com/dylanc/retrofit/coroutines/Dialogs.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.coroutines 4 | 5 | import android.app.Dialog 6 | import androidx.fragment.app.DialogFragment 7 | import androidx.fragment.app.Fragment 8 | import androidx.fragment.app.FragmentActivity 9 | import androidx.fragment.app.FragmentManager 10 | import androidx.lifecycle.LifecycleOwner 11 | import androidx.lifecycle.LiveData 12 | 13 | fun LiveData.observe(activity: FragmentActivity, dialogFragment: DialogFragment) = 14 | observe(activity) { dialogFragment.show(activity.supportFragmentManager, it) } 15 | 16 | fun LiveData.observe(fragment: Fragment, dialogFragment: DialogFragment) = 17 | observe(fragment.viewLifecycleOwner) { dialogFragment.show(fragment.parentFragmentManager, it) } 18 | 19 | fun LiveData.observe(lifecycleOwner: LifecycleOwner, dialog: Dialog?) { 20 | observe(lifecycleOwner) { dialog.show(it) } 21 | } 22 | 23 | fun Dialog?.show(isLoading: Boolean) { 24 | if (isLoading && this?.isShowing != true) { 25 | this?.show() 26 | } else if (!isLoading && this?.isShowing == true) { 27 | this.dismiss() 28 | } 29 | } 30 | 31 | fun DialogFragment.show(fragmentManager: FragmentManager, isShow: Boolean) { 32 | if (isShow && !isShowing) { 33 | show(fragmentManager, toString()) 34 | } else if (!isShow && isShowing) { 35 | dismiss() 36 | } 37 | } 38 | 39 | inline val DialogFragment.isShowing: Boolean 40 | get() = dialog?.isShowing == true && !isRemoving 41 | -------------------------------------------------------------------------------- /retrofit-coroutines/src/main/java/com/dylanc/retrofit/coroutines/DownloadApi.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.coroutines 4 | 5 | import okhttp3.ResponseBody 6 | import retrofit2.http.GET 7 | import retrofit2.http.Header 8 | import retrofit2.http.Streaming 9 | import retrofit2.http.Url 10 | 11 | suspend fun DownloadApi.download(url: String, startByte: Int) = 12 | download(url, "bytes=$startByte-") 13 | 14 | suspend fun DownloadApi.download(url: String, vararg bytesRange: Pair): ResponseBody { 15 | val stringBuilder = StringBuilder() 16 | for ((start, end) in bytesRange) { 17 | if (stringBuilder.isBlank()) { 18 | stringBuilder.append("bytes=${start ?: ""}-${end ?: ""}") 19 | } else { 20 | stringBuilder.append(",${start ?: ""}-${end ?: ""}") 21 | } 22 | } 23 | return download(url, stringBuilder.toString()) 24 | } 25 | 26 | interface DownloadApi { 27 | 28 | @Streaming 29 | @GET 30 | suspend fun download(@Url url: String): ResponseBody 31 | 32 | @Streaming 33 | @GET 34 | suspend fun download(@Url url: String, @Header("Range") range: String): ResponseBody 35 | } 36 | -------------------------------------------------------------------------------- /retrofit-coroutines/src/main/java/com/dylanc/retrofit/coroutines/Flow.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.coroutines 4 | 5 | import kotlinx.coroutines.flow.* 6 | 7 | fun Flow.showLoadingWith(flow: MutableSharedFlow) = 8 | onStart { flow.emit(true) } 9 | .onCompletion { flow.emit(false) } 10 | 11 | fun Flow.catchWith(flow: MutableSharedFlow) = 12 | catch { flow.emit(it) } 13 | -------------------------------------------------------------------------------- /retrofit-coroutines/src/main/java/com/dylanc/retrofit/coroutines/RequestViewModel.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName") 2 | 3 | package com.dylanc.retrofit.coroutines 4 | 5 | import androidx.lifecycle.ViewModel 6 | import com.dylanc.retrofit.coroutines.exception.RequestExceptionObserver 7 | import com.dylanc.retrofit.coroutines.exception.defaultExceptionObserver 8 | import com.dylanc.retrofit.coroutines.loading.RequestLoadingObserver 9 | import com.dylanc.retrofit.coroutines.loading.defaultLoadingObserver 10 | import kotlinx.coroutines.flow.MutableSharedFlow 11 | import kotlinx.coroutines.flow.SharedFlow 12 | 13 | fun initRequestViewModel( 14 | loadingObserver: RequestLoadingObserver, 15 | exceptionObserver: RequestExceptionObserver? = null 16 | ) { 17 | defaultLoadingObserver = loadingObserver 18 | exceptionObserver?.let { defaultExceptionObserver = it } 19 | } 20 | 21 | abstract class RequestViewModel : ViewModel() { 22 | protected val _loadingFlow = MutableSharedFlow() 23 | val loadingFlow: SharedFlow get() = _loadingFlow 24 | 25 | protected val _exceptionFlow = MutableSharedFlow() 26 | val exceptionFlow: SharedFlow get() = _exceptionFlow 27 | } 28 | -------------------------------------------------------------------------------- /retrofit-coroutines/src/main/java/com/dylanc/retrofit/coroutines/RequestViewModelLazy.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.coroutines 4 | 5 | import androidx.annotation.MainThread 6 | import androidx.fragment.app.Fragment 7 | import androidx.fragment.app.FragmentActivity 8 | import androidx.lifecycle.* 9 | import com.dylanc.retrofit.coroutines.exception.defaultExceptionObserver 10 | import com.dylanc.retrofit.coroutines.loading.defaultLoadingObserver 11 | import kotlinx.coroutines.flow.launchIn 12 | import kotlinx.coroutines.flow.onEach 13 | import kotlin.reflect.KClass 14 | 15 | @MainThread 16 | inline fun FragmentActivity.requestViewModels( 17 | observeLoading: Boolean = true, 18 | observeException: Boolean = true, 19 | noinline storeProducer: () -> ViewModelStore = { viewModelStore }, 20 | noinline factoryProducer: () -> ViewModelProvider.Factory = { defaultViewModelProviderFactory }, 21 | ): Lazy = 22 | RequestViewModelLazy(VM::class, { this }, { this }, 23 | storeProducer, factoryProducer, observeLoading, observeException 24 | ) 25 | 26 | @MainThread 27 | inline fun Fragment.requestViewModels( 28 | observeLoading: Boolean = true, 29 | observeException: Boolean = true, 30 | noinline storeProducer: () -> ViewModelStore = { viewModelStore }, 31 | noinline factoryProducer: () -> ViewModelProvider.Factory = { defaultViewModelProviderFactory }, 32 | ): Lazy = 33 | RequestViewModelLazy(VM::class, { viewLifecycleOwner }, { requireActivity() }, 34 | storeProducer, factoryProducer, observeLoading, observeException 35 | ) 36 | 37 | class RequestViewModelLazy( 38 | private val viewModelClass: KClass, 39 | private val lifecycleOwnerProducer: () -> LifecycleOwner, 40 | private val activityProducer: () -> FragmentActivity, 41 | private val storeProducer: () -> ViewModelStore, 42 | private val factoryProducer: () -> ViewModelProvider.Factory, 43 | private val observeLoading: Boolean = true, 44 | private val observeException: Boolean = true, 45 | ) : Lazy { 46 | private var cached: VM? = null 47 | 48 | override val value: VM 49 | get() { 50 | val viewModel = cached 51 | return if (viewModel == null) { 52 | val factory = factoryProducer() 53 | val store = storeProducer() 54 | val activity = activityProducer() 55 | val lifecycleOwner = lifecycleOwnerProducer() 56 | ViewModelProvider(store, factory).get(viewModelClass.java).also { vm -> 57 | if (observeLoading) { 58 | defaultLoadingObserver?.let { observer -> 59 | lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { 60 | override fun onCreate(owner: LifecycleOwner) { 61 | observer.onCreate(activity) 62 | } 63 | 64 | override fun onDestroy(owner: LifecycleOwner) { 65 | observer.onDestroy() 66 | } 67 | }) 68 | vm.loadingFlow.flowWithLifecycle(lifecycleOwner.lifecycle) 69 | .onEach { observer.onChanged(activity, it) } 70 | .launchIn(lifecycleOwner.lifecycleScope) 71 | } 72 | } 73 | if (observeException) { 74 | vm.exceptionFlow.flowWithLifecycle(lifecycleOwner.lifecycle) 75 | .onEach { defaultExceptionObserver.onChanged(activity, it) } 76 | .launchIn(lifecycleOwner.lifecycleScope) 77 | } 78 | cached = vm 79 | } 80 | } else { 81 | viewModel 82 | } 83 | } 84 | 85 | override fun isInitialized() = cached != null 86 | } 87 | -------------------------------------------------------------------------------- /retrofit-coroutines/src/main/java/com/dylanc/retrofit/coroutines/exception/RequestExceptionObserver.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.coroutines.exception 2 | 3 | import android.widget.Toast 4 | import androidx.fragment.app.FragmentActivity 5 | 6 | internal var defaultExceptionObserver = RequestExceptionObserver { activity, e -> 7 | Toast.makeText(activity, e.message, Toast.LENGTH_SHORT).show() 8 | } 9 | 10 | fun interface RequestExceptionObserver { 11 | fun onChanged(activity: FragmentActivity, e: Throwable) 12 | } 13 | -------------------------------------------------------------------------------- /retrofit-coroutines/src/main/java/com/dylanc/retrofit/coroutines/loading/RequestDialogFactory.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.coroutines.loading 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import androidx.fragment.app.FragmentActivity 6 | import com.dylanc.retrofit.coroutines.show 7 | 8 | /** 9 | * @author Dylan Cai 10 | */ 11 | abstract class RequestDialogFactory : RequestLoadingObserver() { 12 | private var dialog: Dialog? = null 13 | 14 | override fun onCreate(activity: FragmentActivity) { 15 | if (dialog == null) { 16 | dialog = create(activity) 17 | } 18 | } 19 | 20 | override fun onChanged(activity: FragmentActivity, isLoading: Boolean) { 21 | dialog?.show(isLoading) 22 | } 23 | 24 | override fun onDestroy() { 25 | dialog = null 26 | } 27 | 28 | abstract fun create(context: Context): Dialog 29 | } 30 | -------------------------------------------------------------------------------- /retrofit-coroutines/src/main/java/com/dylanc/retrofit/coroutines/loading/RequestDialogFragmentFactory.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.coroutines.loading 2 | 3 | import androidx.fragment.app.DialogFragment 4 | import androidx.fragment.app.FragmentActivity 5 | import com.dylanc.retrofit.coroutines.show 6 | 7 | /** 8 | * @author Dylan Cai 9 | */ 10 | abstract class RequestDialogFragmentFactory : RequestLoadingObserver() { 11 | private var dialogFragment: DialogFragment? = null 12 | 13 | override fun onCreate(activity: FragmentActivity) { 14 | if (dialogFragment == null) { 15 | dialogFragment = create() 16 | } 17 | } 18 | 19 | override fun onChanged(activity: FragmentActivity, isLoading: Boolean) { 20 | dialogFragment?.show(activity.supportFragmentManager, isLoading) 21 | } 22 | 23 | override fun onDestroy() { 24 | dialogFragment = null 25 | } 26 | 27 | abstract fun create(): DialogFragment 28 | } 29 | -------------------------------------------------------------------------------- /retrofit-coroutines/src/main/java/com/dylanc/retrofit/coroutines/loading/RequestLoadingObserver.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.coroutines.loading 2 | 3 | import androidx.fragment.app.FragmentActivity 4 | 5 | internal var defaultLoadingObserver: RequestLoadingObserver? = null 6 | 7 | abstract class RequestLoadingObserver { 8 | abstract fun onCreate(activity: FragmentActivity) 9 | 10 | abstract fun onChanged(activity: FragmentActivity, isLoading: Boolean) 11 | 12 | abstract fun onDestroy() 13 | } 14 | -------------------------------------------------------------------------------- /retrofit-ktx/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /retrofit-ktx/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'com.github.dcendents.android-maven' 4 | 5 | group = 'com.github.DylanCaiCoding' 6 | 7 | android { 8 | compileSdkVersion 30 9 | 10 | defaultConfig { 11 | minSdkVersion 21 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | consumerProguardFiles "consumer-rules.pro" 14 | } 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | 23 | compileOptions { 24 | kotlinOptions.freeCompilerArgs += ['-module-name', "retrofit_ktx"] 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 31 | implementation 'com.github.getActivity:GsonFactory:5.2' 32 | api "com.squareup.okhttp3:okhttp:$okhttp_version" 33 | api "com.squareup.okhttp3:logging-interceptor:$okhttp_version" 34 | api "com.squareup.retrofit2:retrofit:$retrofit_version" 35 | api "com.squareup.retrofit2:converter-gson:$retrofit_version" 36 | } -------------------------------------------------------------------------------- /retrofit-ktx/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -dontwarn com.dylanc.retrofit.cookiejar.** 2 | -keep class com.dylanc.retrofit.cookiejar.** 3 | 4 | -keepclassmembers class * implements java.io.Serializable { 5 | static final long serialVersionUID; 6 | private static final java.io.ObjectStreamField[] serialPersistentFields; 7 | !static !transient ; 8 | private void writeObject(java.io.ObjectOutputStream); 9 | private void readObject(java.io.ObjectInputStream); 10 | java.lang.Object writeReplace(); 11 | java.lang.Object readResolve(); 12 | } -------------------------------------------------------------------------------- /retrofit-ktx/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /retrofit-ktx/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/Retrofit.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit 4 | 5 | import okhttp3.OkHttpClient 6 | import okhttp3.Request 7 | import retrofit2.Invocation 8 | import retrofit2.Retrofit 9 | 10 | inline fun retrofit(crossinline block: Retrofit.Builder.() -> Unit): Retrofit = 11 | Retrofit.Builder().apply(block).build() 12 | 13 | inline fun Retrofit.Builder.okHttpClient(crossinline block: OkHttpClient.Builder.() -> Unit): Retrofit.Builder = 14 | client(OkHttpClient.Builder().apply(block).build()) 15 | 16 | inline fun Retrofit.copy(crossinline block: Retrofit.Builder.() -> Unit): Retrofit = 17 | newBuilder().apply(block).build() 18 | 19 | inline fun Request.getMethodAnnotation(): T? = 20 | tag(Invocation::class.java)?.method()?.getAnnotation(T::class.java) 21 | -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/RetrofitCache.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit 4 | 5 | import com.hjq.gson.factory.GsonFactory 6 | import retrofit2.Retrofit 7 | import retrofit2.converter.gson.GsonConverterFactory 8 | 9 | internal lateinit var defaultRetrofit: Retrofit 10 | private val retrofitsCache = mutableMapOf, Retrofit>() 11 | 12 | inline fun initRetrofit(crossinline block: Retrofit.Builder.() -> Unit) = 13 | initRetrofit(retrofit { 14 | apply(block).addConverterFactory(GsonConverterFactory.create(GsonFactory.getSingletonGson())) 15 | }) 16 | 17 | inline fun initRetrofit(retrofit: Retrofit, crossinline block: Retrofit.Builder.() -> Unit) = 18 | initRetrofit(retrofit.copy(block)) 19 | 20 | fun initRetrofit(retrofit: Retrofit) { 21 | if (::defaultRetrofit.isInitialized) { 22 | throw IllegalStateException("Don't allow the default retrofit to reinitialize.") 23 | } 24 | defaultRetrofit = retrofit 25 | } 26 | 27 | inline fun apiServices() = apiServices(T::class.java) 28 | 29 | fun apiServices(service: Class) = lazy { 30 | if (!::defaultRetrofit.isInitialized) { 31 | throw IllegalStateException("Please initialize default retrofit.") 32 | } 33 | defaultRetrofit.create(service, useApiUrl = true) 34 | } 35 | 36 | inline fun Retrofit.create(useApiUrl: Boolean): T = 37 | create(T::class.java, useApiUrl) 38 | 39 | fun Retrofit.create(service: Class, useApiUrl: Boolean): T = 40 | if (useApiUrl) { 41 | val apiUrl = service.getAnnotation(ApiUrl::class.java) 42 | if (apiUrl != null && apiUrl.value.isNotEmpty()) { 43 | retrofitsCache.getOrPut(Retrofit::class.java) { this.copy { baseUrl(apiUrl.value) } } 44 | } else { 45 | this 46 | } 47 | } else { 48 | this 49 | }.create(service) 50 | 51 | @Retention(AnnotationRetention.RUNTIME) 52 | @Target(AnnotationTarget.CLASS) 53 | annotation class ApiUrl(val value: String) 54 | -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/body/MultipartBody.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("PartUtils") 2 | @file:Suppress("unused") 3 | 4 | package com.dylanc.retrofit.body 5 | 6 | import android.content.Context 7 | import android.net.Uri 8 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 9 | import okhttp3.MultipartBody 10 | import okhttp3.RequestBody.Companion.asRequestBody 11 | import java.io.File 12 | 13 | @JvmOverloads 14 | @JvmName("create") 15 | fun File.asPart(name: String, contentType: String = "multipart/form-data"): MultipartBody.Part { 16 | val requestBody = asRequestBody(contentType.toMediaTypeOrNull()) 17 | return MultipartBody.Part.createFormData(name, this.name, requestBody) 18 | } 19 | 20 | @JvmOverloads 21 | @JvmName("create") 22 | fun Uri.asPart( 23 | context: Context, name: String, filename: String? = null, 24 | contentType: String = "multipart/form-data", 25 | offset: Int = 0, byteCount: Int? = null 26 | ): MultipartBody.Part { 27 | val requestBody = asRequestBody(context, contentType, offset, byteCount) 28 | return MultipartBody.Part.createFormData(name, filename, requestBody) 29 | } 30 | -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/body/RequestBody.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("RequestBodyUtils") 2 | 3 | package com.dylanc.retrofit.body 4 | 5 | import android.content.Context 6 | import android.net.Uri 7 | import okhttp3.MediaType 8 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 9 | import okhttp3.RequestBody 10 | import okhttp3.RequestBody.Companion.toRequestBody 11 | 12 | @JvmOverloads 13 | @JvmName("create") 14 | fun Uri.asRequestBody( 15 | context: Context, 16 | contentType: String = "multipart/form-data", 17 | offset: Int = 0, 18 | byteCount: Int? = null 19 | ): RequestBody = 20 | asRequestBody(context, contentType.toMediaTypeOrNull(), offset, byteCount) 21 | 22 | @JvmOverloads 23 | @JvmName("create") 24 | fun Uri.asRequestBody( 25 | context: Context, 26 | mediaType: MediaType?, 27 | offset: Int = 0, 28 | byteCount: Int? = null 29 | ): RequestBody { 30 | val inputStream = context.contentResolver.openInputStream(this) 31 | checkNotNull(inputStream) { "The uri cannot create input stream." } 32 | val content = inputStream.readBytes() 33 | return content.toRequestBody(mediaType, offset, byteCount ?: content.size) 34 | } 35 | -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/body/RequestMap.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.body 4 | 5 | typealias RequestMap = @JvmSuppressWildcards Map 6 | 7 | fun requestMapOf(vararg pairs: Pair) = mutableMapOf(*pairs) -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/body/ResponseBody.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("ResponseBodyUtils") 2 | @file:Suppress("unused") 3 | 4 | package com.dylanc.retrofit.body 5 | 6 | import android.content.Context 7 | import android.net.Uri 8 | import okhttp3.ResponseBody 9 | import java.io.File 10 | 11 | fun ResponseBody.toFile(block: () -> File): File = 12 | use { 13 | block().apply { 14 | outputStream().use { byteStream().copyTo(it) } 15 | } 16 | } 17 | 18 | fun ResponseBody.toUri(context: Context, block: () -> Uri): Uri = 19 | use { 20 | block().apply { 21 | val outputStream = context.contentResolver.openOutputStream(this) 22 | checkNotNull(outputStream) { "The uri cannot create output stream." } 23 | use { byteStream().copyTo(outputStream) } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/cookiejar/PersistentCookieJar.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.cookiejar 4 | 5 | import android.content.Context 6 | import android.content.SharedPreferences 7 | import android.util.Log 8 | import com.dylanc.retrofit.cookiejar.PersistentCookieJar.IdentifiableCookie.Companion.decorateAll 9 | import okhttp3.Cookie 10 | import okhttp3.CookieJar 11 | import okhttp3.HttpUrl 12 | import okhttp3.OkHttpClient 13 | import okhttp3.internal.and 14 | import java.io.* 15 | import java.util.* 16 | 17 | fun OkHttpClient.Builder.persistentCookieJar(context: Context) = 18 | cookieJar(PersistentCookieJarFactory.create(context)) 19 | 20 | fun clearPersistentCookieJar() = PersistentCookieJarFactory.clear() 21 | 22 | object PersistentCookieJarFactory { 23 | private lateinit var persistentCookieJar: PersistentCookieJar 24 | 25 | @JvmStatic 26 | fun create(context: Context) = PersistentCookieJar(context).also { persistentCookieJar = it } 27 | 28 | @JvmStatic 29 | fun clear() = persistentCookieJar.clear() 30 | } 31 | 32 | class PersistentCookieJar(private val sharedPreferences: SharedPreferences) : CookieJar { 33 | 34 | private val cookies: MutableSet = HashSet() 35 | 36 | private val iterator: MutableIterator = cookies.iterator() 37 | 38 | constructor(context: Context) : 39 | this(context.getSharedPreferences("CookiePersistence", Context.MODE_PRIVATE)) 40 | 41 | init { 42 | cookies.addAll(sharedPreferences.loadAll()) 43 | } 44 | 45 | @Synchronized 46 | override fun saveFromResponse(url: HttpUrl, cookies: List) { 47 | this.cookies.addAll(cookies) 48 | sharedPreferences.saveAll(filterPersistentCookies(cookies)) 49 | } 50 | 51 | @Synchronized 52 | override fun loadForRequest(url: HttpUrl): List { 53 | val cookiesToRemove: MutableList = ArrayList() 54 | val validCookies: MutableList = ArrayList() 55 | val it = cookies.iterator() 56 | while (it.hasNext()) { 57 | val currentCookie = it.next().cookie 58 | if (isCookieExpired(currentCookie)) { 59 | cookiesToRemove.add(currentCookie) 60 | it.remove() 61 | } else if (currentCookie.matches(url)) { 62 | validCookies.add(currentCookie) 63 | } 64 | } 65 | sharedPreferences.removeAll(cookiesToRemove) 66 | return validCookies 67 | } 68 | 69 | @Synchronized 70 | fun clearSession() { 71 | cookies.clear() 72 | cookies.addAll(sharedPreferences.loadAll()) 73 | } 74 | 75 | @Synchronized 76 | fun clear() { 77 | cookies.clear() 78 | sharedPreferences.clear() 79 | } 80 | 81 | private fun MutableSet.addAll(cookies: Collection) { 82 | for (cookie in decorateAll(cookies)) { 83 | remove(cookie) 84 | add(cookie) 85 | } 86 | } 87 | 88 | private fun SharedPreferences.loadAll(): List { 89 | val cookies: MutableList = ArrayList(all.size) 90 | for ((_, value) in all) { 91 | val serializedCookie = value as String 92 | val cookie = SerializableCookie().decode(serializedCookie) 93 | if (cookie != null) { 94 | cookies.add(cookie) 95 | } 96 | } 97 | return cookies 98 | } 99 | 100 | private fun SharedPreferences.saveAll(cookies: Collection) { 101 | val editor = edit() 102 | for (cookie in cookies) { 103 | editor.putString(createCookieKey(cookie), SerializableCookie().encode(cookie)) 104 | } 105 | editor.apply() 106 | } 107 | 108 | private fun SharedPreferences.removeAll(cookies: Collection) { 109 | val editor = edit() 110 | for (cookie in cookies) { 111 | editor.remove(createCookieKey(cookie)) 112 | } 113 | editor.apply() 114 | } 115 | 116 | private fun SharedPreferences.clear() { 117 | edit().clear().apply() 118 | } 119 | 120 | private fun filterPersistentCookies(cookies: List): List { 121 | val persistentCookies: MutableList = ArrayList() 122 | for (cookie in cookies) { 123 | if (cookie.persistent) { 124 | persistentCookies.add(cookie) 125 | } 126 | } 127 | return persistentCookies 128 | } 129 | 130 | private fun isCookieExpired(cookie: Cookie): Boolean { 131 | return cookie.expiresAt < System.currentTimeMillis() 132 | } 133 | 134 | private fun createCookieKey(cookie: Cookie): String { 135 | return (if (cookie.secure) "https" else "http") + "://" + cookie.domain + cookie.path + "|" + cookie.name 136 | } 137 | 138 | private class IdentifiableCookie(val cookie: Cookie) { 139 | override fun equals(other: Any?): Boolean = 140 | other is IdentifiableCookie && other.cookie.name == cookie.name && other.cookie.domain == cookie.domain && 141 | other.cookie.path == cookie.path && other.cookie.secure == cookie.secure && other.cookie.hostOnly == cookie.hostOnly 142 | 143 | override fun hashCode(): Int { 144 | var hash = 17 145 | hash = 31 * hash + cookie.name.hashCode() 146 | hash = 31 * hash + cookie.domain.hashCode() 147 | hash = 31 * hash + cookie.path.hashCode() 148 | hash = 31 * hash + if (cookie.secure) 0 else 1 149 | hash = 31 * hash + if (cookie.hostOnly) 0 else 1 150 | return hash 151 | } 152 | 153 | companion object { 154 | @JvmStatic 155 | fun decorateAll(cookies: Collection): List = 156 | cookies.map { IdentifiableCookie(it) } 157 | } 158 | } 159 | } 160 | 161 | class SerializableCookie : Serializable { 162 | @Transient 163 | private var cookie: Cookie? = null 164 | 165 | fun encode(cookie: Cookie?): String? { 166 | this.cookie = cookie 167 | val byteArrayOutputStream = ByteArrayOutputStream() 168 | var objectOutputStream: ObjectOutputStream? = null 169 | try { 170 | objectOutputStream = ObjectOutputStream(byteArrayOutputStream) 171 | objectOutputStream.writeObject(this) 172 | } catch (e: IOException) { 173 | Log.d(TAG, "IOException in encodeCookie", e) 174 | return null 175 | } finally { 176 | if (objectOutputStream != null) { 177 | try { 178 | // Closing a ByteArrayOutputStream has no effect, it can be used later (and is used in the return statement) 179 | objectOutputStream.close() 180 | } catch (e: IOException) { 181 | Log.d(TAG, "Stream not closed in encodeCookie", e) 182 | } 183 | } 184 | } 185 | return byteArrayToHexString(byteArrayOutputStream.toByteArray()) 186 | } 187 | 188 | fun decode(encodedCookie: String): Cookie? { 189 | val bytes = hexStringToByteArray(encodedCookie) 190 | val byteArrayInputStream = ByteArrayInputStream( 191 | bytes 192 | ) 193 | var cookie: Cookie? = null 194 | var objectInputStream: ObjectInputStream? = null 195 | try { 196 | objectInputStream = ObjectInputStream(byteArrayInputStream) 197 | cookie = (objectInputStream.readObject() as SerializableCookie).cookie 198 | } catch (e: IOException) { 199 | Log.d(TAG, "IOException in decodeCookie", e) 200 | } catch (e: ClassNotFoundException) { 201 | Log.d(TAG, "ClassNotFoundException in decodeCookie", e) 202 | } finally { 203 | if (objectInputStream != null) { 204 | try { 205 | objectInputStream.close() 206 | } catch (e: IOException) { 207 | Log.d(TAG, "Stream not closed in decodeCookie", e) 208 | } 209 | } 210 | } 211 | return cookie 212 | } 213 | 214 | @Throws(IOException::class) 215 | private fun writeObject(out: ObjectOutputStream) { 216 | out.writeObject(cookie!!.name) 217 | out.writeObject(cookie!!.value) 218 | out.writeLong(if (cookie!!.persistent) cookie!!.expiresAt else NON_VALID_EXPIRES_AT) 219 | out.writeObject(cookie!!.domain) 220 | out.writeObject(cookie!!.path) 221 | out.writeBoolean(cookie!!.secure) 222 | out.writeBoolean(cookie!!.httpOnly) 223 | out.writeBoolean(cookie!!.hostOnly) 224 | } 225 | 226 | @Throws(IOException::class, ClassNotFoundException::class) 227 | private fun readObject(`in`: ObjectInputStream) { 228 | val builder = Cookie.Builder() 229 | builder.name((`in`.readObject() as String)) 230 | builder.value((`in`.readObject() as String)) 231 | val expiresAt = `in`.readLong() 232 | if (expiresAt != NON_VALID_EXPIRES_AT) { 233 | builder.expiresAt(expiresAt) 234 | } 235 | val domain = `in`.readObject() as String 236 | builder.domain(domain) 237 | builder.path((`in`.readObject() as String)) 238 | if (`in`.readBoolean()) builder.secure() 239 | if (`in`.readBoolean()) builder.httpOnly() 240 | if (`in`.readBoolean()) builder.hostOnlyDomain(domain) 241 | cookie = builder.build() 242 | } 243 | 244 | companion object { 245 | private val TAG = SerializableCookie::class.java.simpleName 246 | private const val serialVersionUID = -8594045714036645534L 247 | 248 | /** 249 | * Using some super basic byte array <-> hex conversions so we don't 250 | * have to rely on any large Base64 libraries. Can be overridden if you 251 | * like! 252 | * 253 | * @param bytes byte array to be converted 254 | * @return string containing hex values 255 | */ 256 | private fun byteArrayToHexString(bytes: ByteArray): String { 257 | val sb = StringBuilder(bytes.size * 2) 258 | for (element in bytes) { 259 | val v: Int = element and 0xff 260 | if (v < 16) { 261 | sb.append('0') 262 | } 263 | sb.append(Integer.toHexString(v)) 264 | } 265 | return sb.toString() 266 | } 267 | 268 | /** 269 | * Converts hex values from strings to byte array 270 | * 271 | * @param hexString string of hex-encoded values 272 | * @return decoded byte array 273 | */ 274 | private fun hexStringToByteArray(hexString: String): ByteArray { 275 | val len = hexString.length 276 | val data = ByteArray(len / 2) 277 | var i = 0 278 | while (i < len) { 279 | data[i / 2] = ((Character.digit(hexString[i], 16) shl 4) + Character 280 | .digit(hexString[i + 1], 16)).toByte() 281 | i += 2 282 | } 283 | return data 284 | } 285 | 286 | private const val NON_VALID_EXPIRES_AT = -1L 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/helper/RetrofitHelper.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.helper 4 | 5 | import android.content.Context 6 | import android.util.Log 7 | import com.dylanc.retrofit.cookiejar.PersistentCookieJarFactory 8 | import com.dylanc.retrofit.cookiejar.persistentCookieJar 9 | import com.dylanc.retrofit.create 10 | import com.dylanc.retrofit.defaultRetrofit 11 | import com.dylanc.retrofit.initRetrofit 12 | import com.dylanc.retrofit.interceptor.* 13 | import com.hjq.gson.factory.GsonFactory 14 | import okhttp3.* 15 | import okhttp3.CacheControl 16 | import okhttp3.logging.HttpLoggingInterceptor 17 | import retrofit2.CallAdapter 18 | import retrofit2.Converter 19 | import retrofit2.Retrofit 20 | import retrofit2.converter.gson.GsonConverterFactory 21 | import java.io.File 22 | import java.util.concurrent.TimeUnit 23 | 24 | /** 25 | * @author Dylan Cai 26 | */ 27 | object RetrofitHelper { 28 | 29 | @JvmStatic 30 | fun getDefault() = Builder() 31 | 32 | @JvmStatic 33 | @JvmOverloads 34 | fun create(service: Class, useApiUrl: Boolean = true): T = 35 | defaultRetrofit.create(service, useApiUrl) 36 | 37 | @JvmStatic 38 | fun putDomain(name: String, url: String) { 39 | retrofitDomains[name] = url 40 | } 41 | 42 | @JvmStatic 43 | fun clearPersistentCookieJar() = 44 | PersistentCookieJarFactory.clear() 45 | 46 | class Builder { 47 | private val headers = mutableMapOf() 48 | private val okHttpClientBuilder = OkHttpClient.Builder() 49 | private var retrofitBuilder = Retrofit.Builder() 50 | 51 | fun migrateFrom(retrofit: Retrofit) { 52 | retrofitBuilder = retrofit.newBuilder() 53 | } 54 | 55 | fun baseUrl(baseUrl: String) = apply { 56 | retrofitBuilder.baseUrl(baseUrl) 57 | } 58 | 59 | fun putDomain(name: String, url: String) = apply { 60 | retrofitDomains[name] = url 61 | } 62 | 63 | fun multipleDomains() = apply { 64 | okHttpClientBuilder.multipleDomains() 65 | } 66 | 67 | fun addHeader(name: String, value: String) = apply { 68 | headers[name] = value 69 | } 70 | 71 | fun addHeaders(headers: Map) = apply { 72 | this.headers.putAll(headers) 73 | } 74 | 75 | fun addHeaders(block: (Request) -> Map) = 76 | addInterceptor(object : HeadersInterceptor() { 77 | override fun onCreateHeaders(request: Request) = block(request) 78 | }) 79 | 80 | fun connectTimeout(connectTimeout: Long, unit: TimeUnit) = apply { 81 | okHttpClientBuilder.connectTimeout(connectTimeout, unit) 82 | } 83 | 84 | fun writeTimeout(writeTimeout: Long, unit: TimeUnit) = apply { 85 | okHttpClientBuilder.connectTimeout(writeTimeout, unit) 86 | } 87 | 88 | fun readTimeout(readTimeout: Long, unit: TimeUnit) = apply { 89 | okHttpClientBuilder.connectTimeout(readTimeout, unit) 90 | } 91 | 92 | fun retryOnConnectionFailure(retryOnConnectionFailure: Boolean) = apply { 93 | okHttpClientBuilder.retryOnConnectionFailure(retryOnConnectionFailure) 94 | } 95 | 96 | fun authenticator(authenticator: Authenticator) = apply { 97 | okHttpClientBuilder.authenticator(authenticator) 98 | } 99 | 100 | fun cacheControl( 101 | context: Context, 102 | maxSize: Long = 10L * 1024 * 1024, 103 | block: CacheControl.Builder.(Request) -> Unit = {} 104 | ) = apply { 105 | okHttpClientBuilder.cacheControl(context, maxSize, block) 106 | } 107 | 108 | fun cacheControl( 109 | directory: File, 110 | maxSize: Long = 10L * 1024 * 1024, 111 | block: CacheControl.Builder.(Request) -> Unit = {} 112 | ) = apply { 113 | okHttpClientBuilder.cacheControl(directory, maxSize, block) 114 | } 115 | 116 | fun cookieJar(cookieJar: CookieJar) = apply { 117 | okHttpClientBuilder.cookieJar(cookieJar) 118 | } 119 | 120 | fun persistentCookiesJar(context: Context) = apply { 121 | okHttpClientBuilder.persistentCookieJar(context) 122 | } 123 | 124 | fun addInterceptor(interceptor: Interceptor) = apply { 125 | okHttpClientBuilder.addInterceptor(interceptor) 126 | } 127 | 128 | fun addNetworkInterceptor(interceptor: Interceptor) = apply { 129 | okHttpClientBuilder.addNetworkInterceptor(interceptor) 130 | } 131 | 132 | @JvmOverloads 133 | fun printHttpLog( 134 | debug: Boolean = true, 135 | logger: HttpLoggingInterceptor.Logger = HttpLoggingInterceptor.Logger { Log.d("Http", it) }, 136 | level: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BODY, 137 | ) = apply { 138 | okHttpClientBuilder.printHttpLog(debug, level, logger) 139 | } 140 | 141 | fun doOnResponse(block: (Response, String, String) -> Response) = apply { 142 | okHttpClientBuilder.doOnResponse(block) 143 | } 144 | 145 | fun addConverterFactory(factory: Converter.Factory) = apply { 146 | retrofitBuilder.addConverterFactory(factory) 147 | } 148 | 149 | fun addCallAdapterFactory(factory: CallAdapter.Factory) = apply { 150 | retrofitBuilder.addCallAdapterFactory(factory) 151 | } 152 | 153 | fun okHttpClientBuilder(block: Callback) = apply { 154 | block(okHttpClientBuilder) 155 | } 156 | 157 | fun retrofitBuilder(block: Callback) = apply { 158 | block(retrofitBuilder) 159 | } 160 | 161 | fun init() = 162 | initRetrofit( 163 | retrofitBuilder 164 | .client(okHttpClientBuilder.addHeaders(headers).build()) 165 | .addConverterFactory(GsonConverterFactory.create(GsonFactory.getSingletonGson())) 166 | .build() 167 | ) 168 | } 169 | 170 | fun interface Callback { 171 | operator fun invoke(p: P) 172 | } 173 | } -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/interceptor/CacheControlInterceptor.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.interceptor 4 | 5 | import android.content.Context 6 | import com.dylanc.retrofit.getMethodAnnotation 7 | import okhttp3.* 8 | import java.io.File 9 | import java.util.concurrent.TimeUnit 10 | 11 | inline fun cacheControl(crossinline block: okhttp3.CacheControl.Builder.() -> Unit): okhttp3.CacheControl = 12 | okhttp3.CacheControl.Builder().apply(block).build() 13 | 14 | inline fun OkHttpClient.Builder.cacheControl( 15 | context: Context, 16 | maxSize: Long = 10L * 1024 * 1024, 17 | crossinline block: okhttp3.CacheControl.Builder.(Request) -> Unit = {} 18 | ) = 19 | cacheControl(File(context.externalCacheDir, "responses.cache"), maxSize, block) 20 | 21 | inline fun OkHttpClient.Builder.cacheControl( 22 | directory: File, 23 | maxSize: Long = 10L * 1024 * 1024, 24 | crossinline block: okhttp3.CacheControl.Builder.(Request) -> Unit = {} 25 | ) = 26 | apply { 27 | val cacheControlInterceptor = CacheControlInterceptor { cacheControl { block(it) } } 28 | cache(Cache(directory, maxSize)) 29 | addNetworkInterceptor(cacheControlInterceptor) 30 | addInterceptor(cacheControlInterceptor) 31 | } 32 | 33 | @Retention(AnnotationRetention.RUNTIME) 34 | @Target(AnnotationTarget.FUNCTION) 35 | annotation class CacheControl( 36 | val noCache: Boolean = false, 37 | val noStore: Boolean = false, 38 | val maxAge: Int = -1, 39 | val maxStale: Int = -1, 40 | val minFresh: Int = -1, 41 | val onlyIfCached: Boolean = false, 42 | val noTransform: Boolean = false, 43 | val immutable: Boolean = false, 44 | val timeUnit: TimeUnit = TimeUnit.SECONDS 45 | ) 46 | 47 | class CacheControlInterceptor constructor( 48 | private val onCreateCacheControl: (Request) -> okhttp3.CacheControl? 49 | ) : Interceptor { 50 | 51 | override fun intercept(chain: Interceptor.Chain): Response { 52 | val request = chain.request() 53 | val cacheControl = request.getMethodAnnotation()?.let { 54 | cacheControl { 55 | if (it.noCache) noCache() 56 | if (it.noStore) noStore() 57 | if (it.maxAge >= 0) maxAge(it.maxAge, it.timeUnit) 58 | if (it.maxStale >= 0) maxStale(it.maxStale, it.timeUnit) 59 | if (it.minFresh >= 0) minFresh(it.minFresh, it.timeUnit) 60 | if (it.onlyIfCached) onlyIfCached() 61 | if (it.noTransform) noTransform() 62 | if (it.immutable) immutable() 63 | } 64 | } ?: onCreateCacheControl(request) 65 | return chain.proceed(request).newBuilder() 66 | .apply { 67 | if (cacheControl != null && cacheControl.toString().isNotEmpty()) { 68 | removeHeader("Pragma") 69 | removeHeader("Cache-Control") 70 | header("Cache-Control", cacheControl.toString()) 71 | } 72 | } 73 | .build() 74 | } 75 | } -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/interceptor/DomainsInterceptor.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.interceptor 4 | 5 | import com.dylanc.retrofit.getMethodAnnotation 6 | import okhttp3.HttpUrl.Companion.toHttpUrlOrNull 7 | import okhttp3.Interceptor 8 | import okhttp3.OkHttpClient 9 | import okhttp3.Response 10 | 11 | val retrofitDomains = mutableMapOf() 12 | 13 | fun OkHttpClient.Builder.multipleDomains(vararg pairs: Pair) = 14 | addInterceptor(DomainsInterceptor(retrofitDomains.apply { putAll(pairs) })) 15 | 16 | fun OkHttpClient.Builder.multipleDomains(domains: MutableMap = retrofitDomains) = 17 | addInterceptor(DomainsInterceptor(domains)) 18 | 19 | @Retention(AnnotationRetention.RUNTIME) 20 | @Target(AnnotationTarget.FUNCTION) 21 | annotation class DomainName(val value: String) 22 | 23 | class DomainsInterceptor(private val domains: MutableMap) : Interceptor { 24 | 25 | override fun intercept(chain: Interceptor.Chain): Response { 26 | val request = chain.request() 27 | val domainName = request.getMethodAnnotation()?.value 28 | return if (!domainName.isNullOrBlank()) { 29 | val baseUrl = request.url 30 | val builder = request.newBuilder() 31 | val url = domains[domainName]?.toHttpUrlOrNull() ?: baseUrl 32 | val newFullUrl = request.url.newBuilder() 33 | .apply { 34 | if (domains[domainName] != null) { 35 | for (i in 0 until baseUrl.pathSize) { 36 | removePathSegment(0) 37 | } 38 | (url.encodedPathSegments + baseUrl.encodedPathSegments).forEach { 39 | addEncodedPathSegment(it) 40 | } 41 | } 42 | } 43 | .scheme(url.scheme) 44 | .host(url.host) 45 | .port(url.port) 46 | .build() 47 | chain.proceed(builder.url(newFullUrl).build()) 48 | } else { 49 | chain.proceed(request) 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/interceptor/GlobalHeadersInterceptor.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.interceptor 4 | 5 | import okhttp3.OkHttpClient 6 | import okhttp3.Request 7 | 8 | fun OkHttpClient.Builder.addHeaders(vararg pairs: Pair): OkHttpClient.Builder = 9 | addHeaders(mutableMapOf(*pairs)) 10 | 11 | fun OkHttpClient.Builder.addHeaders(headers: Map): OkHttpClient.Builder = apply { 12 | if (headers.isNotEmpty()) addInterceptor(GlobalHeadersInterceptor(headers)) 13 | } 14 | 15 | class GlobalHeadersInterceptor( 16 | private val headers: Map 17 | ) : HeadersInterceptor() { 18 | 19 | override fun onCreateHeaders(request: Request) = headers 20 | } -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/interceptor/HeadersInterceptor.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.interceptor 4 | 5 | import okhttp3.Interceptor 6 | import okhttp3.OkHttpClient 7 | import okhttp3.Request 8 | import okhttp3.Response 9 | 10 | inline fun OkHttpClient.Builder.addHeaders( 11 | crossinline block: MutableMap.(Request) -> Unit 12 | ) = 13 | addInterceptor(object : HeadersInterceptor() { 14 | override fun onCreateHeaders(request: Request) = 15 | mutableMapOf().apply { block(request) } 16 | }) 17 | 18 | abstract class HeadersInterceptor : Interceptor { 19 | 20 | override fun intercept(chain: Interceptor.Chain): Response { 21 | val request = chain.request() 22 | val newRequest = request.newBuilder() 23 | .method(request.method, request.body) 24 | .apply { 25 | val headers = onCreateHeaders(request) 26 | for ((name, value) in headers) { 27 | header(name, value) 28 | } 29 | } 30 | .build() 31 | return chain.proceed(newRequest) 32 | } 33 | 34 | abstract fun onCreateHeaders(request: Request): Map 35 | } -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/interceptor/HttpLoggingInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.interceptor 2 | 3 | import android.util.Log 4 | import okhttp3.OkHttpClient 5 | import okhttp3.logging.HttpLoggingInterceptor 6 | 7 | fun OkHttpClient.Builder.printHttpLog( 8 | debug: Boolean = true, 9 | level: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BODY, 10 | logger: HttpLoggingInterceptor.Logger = HttpLoggingInterceptor.Logger { Log.d("Http", it) } 11 | ) = apply { 12 | if (debug) addInterceptor(HttpLoggingInterceptor(logger).setLevel(level)) 13 | } 14 | -------------------------------------------------------------------------------- /retrofit-ktx/src/main/java/com/dylanc/retrofit/interceptor/ResponseBodyInterceptor.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.interceptor 4 | 5 | import okhttp3.Interceptor 6 | import okhttp3.OkHttpClient 7 | import okhttp3.Response 8 | import okio.Buffer 9 | import okio.GzipSource 10 | import java.nio.charset.Charset 11 | import java.nio.charset.StandardCharsets 12 | 13 | inline fun OkHttpClient.Builder.doOnResponse(crossinline block: (Response, String, String) -> Response) = 14 | addInterceptor(object : ResponseBodyInterceptor() { 15 | override fun intercept(response: Response, url: String, body: String) = block(response, url, body) 16 | }) 17 | 18 | abstract class ResponseBodyInterceptor : Interceptor { 19 | 20 | override fun intercept(chain: Interceptor.Chain): Response { 21 | val request = chain.request() 22 | val url = request.url.toString() 23 | val response = chain.proceed(request) 24 | response.body?.let { responseBody -> 25 | val contentLength = responseBody.contentLength() 26 | val source = responseBody.source() 27 | source.request(Long.MAX_VALUE) 28 | var buffer = source.buffer 29 | 30 | if ("gzip".equals(response.headers["Content-Encoding"], ignoreCase = true)) { 31 | GzipSource(buffer.clone()).use { gzippedResponseBody -> 32 | buffer = Buffer() 33 | buffer.writeAll(gzippedResponseBody) 34 | } 35 | } 36 | 37 | val contentType = responseBody.contentType() 38 | val charset: Charset = 39 | contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8 40 | if (contentLength != 0L) { 41 | return intercept(response, url, buffer.clone().readString(charset)) 42 | } 43 | } 44 | return response 45 | } 46 | 47 | abstract fun intercept(response: Response, url: String, body: String): Response 48 | } -------------------------------------------------------------------------------- /retrofit-ktx/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Library 3 | 4 | -------------------------------------------------------------------------------- /retrofit-ktx/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /retrofit-rxjava2/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /retrofit-rxjava2/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'com.github.dcendents.android-maven' 5 | 6 | group = 'com.github.DylanCaiCoding' 7 | 8 | android { 9 | compileSdkVersion 30 10 | buildToolsVersion "29.0.3" 11 | 12 | defaultConfig { 13 | minSdkVersion 21 14 | targetSdkVersion 30 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | consumerProguardFiles 'consumer-rules.pro' 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | 29 | compileOptions { 30 | kotlinOptions.freeCompilerArgs += ['-module-name', "retrofit_rxjava2"] 31 | } 32 | kotlinOptions { 33 | jvmTarget = JavaVersion.VERSION_1_8.toString() 34 | } 35 | } 36 | 37 | dependencies { 38 | implementation fileTree(dir: 'libs', include: ['*.jar']) 39 | implementation 'androidx.appcompat:appcompat:1.3.0' 40 | api "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version" 41 | api "io.reactivex.rxjava2:rxandroid:$rxandroid_version" 42 | } -------------------------------------------------------------------------------- /retrofit-rxjava2/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DylanCaiCoding/RetrofitKTX/266af7b26da9abdb0508c6f5a4d4db93645df582/retrofit-rxjava2/consumer-rules.pro -------------------------------------------------------------------------------- /retrofit-rxjava2/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /retrofit-rxjava2/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /retrofit-rxjava2/src/main/java/com/dylanc/retrofit/rxjava2/FileTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.rxjava2 2 | 3 | import io.reactivex.* 4 | import io.reactivex.android.schedulers.AndroidSchedulers 5 | import io.reactivex.schedulers.Schedulers 6 | import okhttp3.ResponseBody 7 | import org.reactivestreams.Publisher 8 | import java.io.File 9 | import java.io.InputStream 10 | 11 | /** 12 | * @author Dylan Cai 13 | */ 14 | class FileTransformer(private val pathname: String) : ObservableTransformer, 15 | FlowableTransformer, SingleTransformer, 16 | MaybeTransformer { 17 | 18 | override fun apply(upstream: Observable): ObservableSource = 19 | upstream.subscribeOn(Schedulers.io()) 20 | .unsubscribeOn(Schedulers.io()) 21 | .map { it.byteStream() } 22 | .observeOn(Schedulers.computation()) 23 | .map { it.toFile(pathname) } 24 | .observeOn(AndroidSchedulers.mainThread()) 25 | 26 | override fun apply(upstream: Flowable): Publisher = 27 | upstream.subscribeOn(Schedulers.io()) 28 | .unsubscribeOn(Schedulers.io()) 29 | .map { it.byteStream() } 30 | .observeOn(Schedulers.computation()) 31 | .map { it.toFile(pathname) } 32 | .observeOn(AndroidSchedulers.mainThread()) 33 | 34 | override fun apply(upstream: Single): SingleSource = 35 | upstream.subscribeOn(Schedulers.io()) 36 | .unsubscribeOn(Schedulers.io()) 37 | .map { it.byteStream() } 38 | .observeOn(Schedulers.computation()) 39 | .map { it.toFile(pathname) } 40 | .observeOn(AndroidSchedulers.mainThread()) 41 | 42 | override fun apply(upstream: Maybe): MaybeSource = 43 | upstream.subscribeOn(Schedulers.io()) 44 | .unsubscribeOn(Schedulers.io()) 45 | .map { it.byteStream() } 46 | .observeOn(Schedulers.computation()) 47 | .map { it.toFile(pathname) } 48 | .observeOn(AndroidSchedulers.mainThread()) 49 | 50 | private fun InputStream.toFile(pathname: String) = 51 | File(pathname).apply { 52 | use { input -> 53 | outputStream().use { fileOut -> 54 | input.copyTo(fileOut) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /retrofit-rxjava2/src/main/java/com/dylanc/retrofit/rxjava2/LoadingTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.rxjava2 2 | 3 | import io.reactivex.* 4 | import org.reactivestreams.Publisher 5 | 6 | /** 7 | * @author Dylan Cai 8 | */ 9 | class LoadingTransformer( 10 | private val requestLoading: RequestLoading 11 | ) : ObservableTransformer, FlowableTransformer, 12 | SingleTransformer, MaybeTransformer, CompletableTransformer { 13 | 14 | override fun apply(upstream: Observable): Observable = 15 | upstream.doOnSubscribe { requestLoading.onRequestLoading(true) } 16 | .doFinally { requestLoading.onRequestLoading(false) } 17 | 18 | override fun apply(upstream: Flowable): Publisher = 19 | upstream.doOnSubscribe { requestLoading.onRequestLoading(true) } 20 | .doFinally { requestLoading.onRequestLoading(false) } 21 | 22 | override fun apply(upstream: Single): SingleSource = 23 | upstream.doOnSubscribe { requestLoading.onRequestLoading(true) } 24 | .doFinally { requestLoading.onRequestLoading(false) } 25 | 26 | override fun apply(upstream: Maybe): MaybeSource = 27 | upstream.doOnSubscribe { requestLoading.onRequestLoading(true) } 28 | .doFinally { requestLoading.onRequestLoading(false) } 29 | 30 | override fun apply(upstream: Completable): CompletableSource = 31 | upstream.doOnSubscribe { requestLoading.onRequestLoading(true) } 32 | .doFinally { requestLoading.onRequestLoading(false) } 33 | } 34 | -------------------------------------------------------------------------------- /retrofit-rxjava2/src/main/java/com/dylanc/retrofit/rxjava2/RequestLoading.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.rxjava2 4 | 5 | import android.app.Dialog 6 | import androidx.fragment.app.DialogFragment 7 | import androidx.fragment.app.FragmentManager 8 | 9 | /** 10 | * @author Dylan Cai 11 | */ 12 | fun interface RequestLoading { 13 | 14 | fun onRequestLoading(isLoading: Boolean) 15 | 16 | companion object { 17 | const val TAG_LOADING = "request_loading" 18 | 19 | fun create(dialog: Dialog) = RequestLoading { 20 | if (it && !dialog.isShowing) { 21 | dialog.show() 22 | } else if (!it && dialog.isShowing) { 23 | dialog.dismiss() 24 | } 25 | } 26 | 27 | fun create(fragmentManager: FragmentManager, dialogFragment: DialogFragment, tag: String = TAG_LOADING) = 28 | RequestLoading { 29 | if (it && !dialogFragment.isShowing) { 30 | dialogFragment.show(fragmentManager, tag) 31 | } else if (!it && dialogFragment.isShowing) { 32 | dialogFragment.dismiss() 33 | } 34 | } 35 | 36 | private val DialogFragment.isShowing: Boolean 37 | get() = dialog?.isShowing == true && !isRemoving 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /retrofit-rxjava2/src/main/java/com/dylanc/retrofit/rxjava2/RxDownloadApi.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.rxjava2 4 | 5 | import io.reactivex.Single 6 | import okhttp3.ResponseBody 7 | import retrofit2.http.GET 8 | import retrofit2.http.Header 9 | import retrofit2.http.Streaming 10 | import retrofit2.http.Url 11 | 12 | fun RxDownloadApi.download(url: String, startByte: Int) = 13 | download(url, "bytes=$startByte-") 14 | 15 | fun RxDownloadApi.download(url: String, vararg bytesRange: Pair): Single { 16 | val stringBuilder = StringBuilder() 17 | for ((start, end) in bytesRange) { 18 | if (stringBuilder.isBlank()) { 19 | stringBuilder.append("bytes=${start ?: ""}-${end ?: ""}") 20 | } else { 21 | stringBuilder.append(",${start ?: ""}-${end ?: ""}") 22 | } 23 | } 24 | return download(url, stringBuilder.toString()) 25 | } 26 | 27 | interface RxDownloadApi { 28 | 29 | @Streaming 30 | @GET 31 | fun download(@Url url: String): Single 32 | 33 | @Streaming 34 | @GET 35 | fun download(@Url url: String, @Header("Range") range: String): Single 36 | } 37 | -------------------------------------------------------------------------------- /retrofit-rxjava2/src/main/java/com/dylanc/retrofit/rxjava2/ThreadTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.rxjava2 2 | 3 | import io.reactivex.* 4 | import org.reactivestreams.Publisher 5 | 6 | /** 7 | * @author Dylan Cai 8 | */ 9 | class ThreadTransformer( 10 | private val subscribeScheduler: Scheduler, 11 | private val observeScheduler: Scheduler 12 | ) : ObservableTransformer, FlowableTransformer, 13 | SingleTransformer, MaybeTransformer, CompletableTransformer { 14 | 15 | override fun apply(upstream: Observable): ObservableSource = upstream 16 | .subscribeOn(subscribeScheduler) 17 | .observeOn(observeScheduler) 18 | 19 | override fun apply(upstream: Flowable): Publisher = upstream 20 | .subscribeOn(subscribeScheduler) 21 | .observeOn(observeScheduler) 22 | 23 | override fun apply(upstream: Single): SingleSource = upstream 24 | .subscribeOn(subscribeScheduler) 25 | .observeOn(observeScheduler) 26 | 27 | override fun apply(upstream: Maybe): MaybeSource = upstream 28 | .subscribeOn(subscribeScheduler) 29 | .observeOn(observeScheduler) 30 | 31 | override fun apply(upstream: Completable): CompletableSource = upstream 32 | .subscribeOn(subscribeScheduler) 33 | .observeOn(observeScheduler) 34 | } 35 | -------------------------------------------------------------------------------- /retrofit-rxjava2/src/main/java/com/dylanc/retrofit/rxjava2/Transformers.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.rxjava2 4 | 5 | import android.app.Dialog 6 | import androidx.fragment.app.DialogFragment 7 | import androidx.fragment.app.FragmentManager 8 | import com.dylanc.retrofit.rxjava2.RequestLoading.Companion.TAG_LOADING 9 | import io.reactivex.Flowable 10 | import io.reactivex.Observable 11 | import io.reactivex.Single 12 | import io.reactivex.android.schedulers.AndroidSchedulers 13 | import io.reactivex.schedulers.Schedulers 14 | import okhttp3.ResponseBody 15 | import java.io.File 16 | 17 | fun Observable.io2mainThread(): Observable = 18 | compose(Transformers.io2mainThread()) 19 | 20 | fun Flowable.io2mainThread(): Flowable = 21 | compose(Transformers.io2mainThread()) 22 | 23 | fun Single.io2mainThread(): Single = 24 | compose(Transformers.io2mainThread()) 25 | 26 | fun Observable.showLoading(requestLoading: RequestLoading): Observable = 27 | compose(Transformers.showLoading(requestLoading)) 28 | 29 | fun Flowable.showLoading(requestLoading: RequestLoading): Flowable = 30 | compose(Transformers.showLoading(requestLoading)) 31 | 32 | fun Single.showLoading(requestLoading: RequestLoading): Single = 33 | compose(Transformers.showLoading(requestLoading)) 34 | 35 | fun Observable.showLoading(onLoading: (Boolean) -> Unit): Observable = 36 | compose(Transformers.showLoading(onLoading)) 37 | 38 | fun Flowable.showLoading(onLoading: (Boolean) -> Unit): Flowable = 39 | compose(Transformers.showLoading(onLoading)) 40 | 41 | fun Single.showLoading(onLoading: (Boolean) -> Unit): Single = 42 | compose(Transformers.showLoading(onLoading)) 43 | 44 | fun Observable.showLoading(fragmentManager: FragmentManager, dialogFragment: DialogFragment): Observable = 45 | compose(Transformers.showLoading(fragmentManager, dialogFragment)) 46 | 47 | fun Flowable.showLoading(fragmentManager: FragmentManager, dialogFragment: DialogFragment): Flowable = 48 | compose(Transformers.showLoading(fragmentManager, dialogFragment)) 49 | 50 | fun Single.showLoading(fragmentManager: FragmentManager, dialogFragment: DialogFragment): Single = 51 | compose(Transformers.showLoading(fragmentManager, dialogFragment)) 52 | 53 | fun Observable.showLoading(dialog: Dialog): Observable = 54 | compose(Transformers.showLoading(dialog)) 55 | 56 | fun Flowable.showLoading(dialog: Dialog): Flowable = 57 | compose(Transformers.showLoading(dialog)) 58 | 59 | fun Single.showLoading(dialog: Dialog): Single = 60 | compose(Transformers.showLoading(dialog)) 61 | 62 | fun Observable.toFile(pathname: String): Observable = 63 | compose(Transformers.toFile(pathname)) 64 | 65 | fun Flowable.toFile(pathname: String): Flowable = 66 | compose(Transformers.toFile(pathname)) 67 | 68 | fun Single.toFile(pathname: String): Single = 69 | compose(Transformers.toFile(pathname)) 70 | 71 | object Transformers { 72 | 73 | @JvmStatic 74 | fun io2mainThread() = 75 | ThreadTransformer(Schedulers.io(), AndroidSchedulers.mainThread()) 76 | 77 | @JvmStatic 78 | fun showLoading(requestLoading: RequestLoading) = 79 | LoadingTransformer(requestLoading) 80 | 81 | @JvmStatic 82 | @JvmOverloads 83 | fun showLoading(fragmentManager: FragmentManager, dialogFragment: DialogFragment, tag: String = TAG_LOADING) = 84 | showLoading(RequestLoading.create(fragmentManager, dialogFragment, tag)) 85 | 86 | @JvmStatic 87 | fun showLoading(dialog: Dialog) = 88 | showLoading(RequestLoading.create(dialog)) 89 | 90 | @JvmStatic 91 | fun toFile(pathname: String): FileTransformer = 92 | FileTransformer(pathname) 93 | } 94 | -------------------------------------------------------------------------------- /retrofit-rxjava3/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /retrofit-rxjava3/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 31 8 | 9 | defaultConfig { 10 | minSdkVersion 21 11 | targetSdkVersion 31 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation 'androidx.appcompat:appcompat:1.3.0' 36 | api "com.squareup.retrofit2:adapter-rxjava3:$retrofit_version" 37 | api "io.reactivex.rxjava3:rxandroid:3.0.0" 38 | testImplementation 'junit:junit:4.+' 39 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 41 | } -------------------------------------------------------------------------------- /retrofit-rxjava3/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DylanCaiCoding/RetrofitKTX/266af7b26da9abdb0508c6f5a4d4db93645df582/retrofit-rxjava3/consumer-rules.pro -------------------------------------------------------------------------------- /retrofit-rxjava3/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /retrofit-rxjava3/src/androidTest/java/com/dylanc/retrofit/rxjava3/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.rxjava3 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.dylanc.retrofit.rxjava3.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /retrofit-rxjava3/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /retrofit-rxjava3/src/main/java/com/dylanc/retrofit/rxjava3/FileTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.rxjava3 2 | 3 | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers 4 | import io.reactivex.rxjava3.core.* 5 | import io.reactivex.rxjava3.schedulers.Schedulers 6 | import okhttp3.ResponseBody 7 | import org.reactivestreams.Publisher 8 | import java.io.File 9 | import java.io.InputStream 10 | 11 | /** 12 | * @author Dylan Cai 13 | */ 14 | class FileTransformer(private val pathname: String) : ObservableTransformer, 15 | FlowableTransformer, SingleTransformer, 16 | MaybeTransformer { 17 | 18 | override fun apply(upstream: Observable): ObservableSource = 19 | upstream.subscribeOn(Schedulers.io()) 20 | .unsubscribeOn(Schedulers.io()) 21 | .map { it.byteStream() } 22 | .observeOn(Schedulers.computation()) 23 | .map { it.toFile(pathname) } 24 | .observeOn(AndroidSchedulers.mainThread()) 25 | 26 | override fun apply(upstream: Flowable): Publisher = 27 | upstream.subscribeOn(Schedulers.io()) 28 | .unsubscribeOn(Schedulers.io()) 29 | .map { it.byteStream() } 30 | .observeOn(Schedulers.computation()) 31 | .map { it.toFile(pathname) } 32 | .observeOn(AndroidSchedulers.mainThread()) 33 | 34 | override fun apply(upstream: Single): SingleSource = 35 | upstream.subscribeOn(Schedulers.io()) 36 | .unsubscribeOn(Schedulers.io()) 37 | .map { it.byteStream() } 38 | .observeOn(Schedulers.computation()) 39 | .map { it.toFile(pathname) } 40 | .observeOn(AndroidSchedulers.mainThread()) 41 | 42 | override fun apply(upstream: Maybe): MaybeSource = 43 | upstream.subscribeOn(Schedulers.io()) 44 | .unsubscribeOn(Schedulers.io()) 45 | .map { it.byteStream() } 46 | .observeOn(Schedulers.computation()) 47 | .map { it.toFile(pathname) } 48 | .observeOn(AndroidSchedulers.mainThread()) 49 | 50 | private fun InputStream.toFile(pathname: String) = 51 | File(pathname).apply { 52 | use { input -> 53 | outputStream().use { fileOut -> 54 | input.copyTo(fileOut) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /retrofit-rxjava3/src/main/java/com/dylanc/retrofit/rxjava3/LoadingTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.rxjava3 2 | 3 | import io.reactivex.rxjava3.core.* 4 | import org.reactivestreams.Publisher 5 | 6 | /** 7 | * @author Dylan Cai 8 | */ 9 | class LoadingTransformer( 10 | private val requestLoading: RequestLoading 11 | ) : ObservableTransformer, FlowableTransformer, 12 | SingleTransformer, MaybeTransformer, CompletableTransformer { 13 | 14 | override fun apply(upstream: Observable): Observable = 15 | upstream.doOnSubscribe { requestLoading.onRequestLoading(true) } 16 | .doFinally { requestLoading.onRequestLoading(false) } 17 | 18 | override fun apply(upstream: Flowable): Publisher = 19 | upstream.doOnSubscribe { requestLoading.onRequestLoading(true) } 20 | .doFinally { requestLoading.onRequestLoading(false) } 21 | 22 | override fun apply(upstream: Single): SingleSource = 23 | upstream.doOnSubscribe { requestLoading.onRequestLoading(true) } 24 | .doFinally { requestLoading.onRequestLoading(false) } 25 | 26 | override fun apply(upstream: Maybe): MaybeSource = 27 | upstream.doOnSubscribe { requestLoading.onRequestLoading(true) } 28 | .doFinally { requestLoading.onRequestLoading(false) } 29 | 30 | override fun apply(upstream: Completable): CompletableSource = 31 | upstream.doOnSubscribe { requestLoading.onRequestLoading(true) } 32 | .doFinally { requestLoading.onRequestLoading(false) } 33 | } 34 | -------------------------------------------------------------------------------- /retrofit-rxjava3/src/main/java/com/dylanc/retrofit/rxjava3/RequestLoading.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.rxjava3 4 | 5 | import android.app.Dialog 6 | import androidx.fragment.app.DialogFragment 7 | import androidx.fragment.app.FragmentManager 8 | 9 | /** 10 | * @author Dylan Cai 11 | */ 12 | fun interface RequestLoading { 13 | 14 | fun onRequestLoading(isLoading: Boolean) 15 | 16 | companion object { 17 | const val TAG_LOADING = "request_loading" 18 | 19 | fun create(dialog: Dialog) = RequestLoading { 20 | if (it && !dialog.isShowing) { 21 | dialog.show() 22 | } else if (!it && dialog.isShowing) { 23 | dialog.dismiss() 24 | } 25 | } 26 | 27 | fun create(fragmentManager: FragmentManager, dialogFragment: DialogFragment, tag: String = TAG_LOADING) = 28 | RequestLoading { 29 | if (it && !dialogFragment.isShowing) { 30 | dialogFragment.show(fragmentManager, tag) 31 | } else if (!it && dialogFragment.isShowing) { 32 | dialogFragment.dismiss() 33 | } 34 | } 35 | 36 | private val DialogFragment.isShowing: Boolean 37 | get() = dialog?.isShowing == true && !isRemoving 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /retrofit-rxjava3/src/main/java/com/dylanc/retrofit/rxjava3/RxDownloadApi.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.rxjava3 4 | 5 | import io.reactivex.rxjava3.core.Single 6 | import okhttp3.ResponseBody 7 | import retrofit2.http.GET 8 | import retrofit2.http.Header 9 | import retrofit2.http.Streaming 10 | import retrofit2.http.Url 11 | 12 | fun RxDownloadApi.download(url: String, startByte: Int) = 13 | download(url, "bytes=$startByte-") 14 | 15 | fun RxDownloadApi.download(url: String, vararg bytesRange: Pair): Single { 16 | val stringBuilder = StringBuilder() 17 | for ((start, end) in bytesRange) { 18 | if (stringBuilder.isBlank()) { 19 | stringBuilder.append("bytes=${start ?: ""}-${end ?: ""}") 20 | } else { 21 | stringBuilder.append(",${start ?: ""}-${end ?: ""}") 22 | } 23 | } 24 | return download(url, stringBuilder.toString()) 25 | } 26 | 27 | interface RxDownloadApi { 28 | 29 | @Streaming 30 | @GET 31 | fun download(@Url url: String): Single 32 | 33 | @Streaming 34 | @GET 35 | fun download(@Url url: String, @Header("Range") range: String): Single 36 | } 37 | -------------------------------------------------------------------------------- /retrofit-rxjava3/src/main/java/com/dylanc/retrofit/rxjava3/ThreadTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.rxjava3 2 | 3 | import io.reactivex.rxjava3.core.* 4 | import org.reactivestreams.Publisher 5 | 6 | /** 7 | * @author Dylan Cai 8 | */ 9 | class ThreadTransformer( 10 | private val subscribeScheduler: Scheduler, 11 | private val observeScheduler: Scheduler 12 | ) : ObservableTransformer, FlowableTransformer, 13 | SingleTransformer, MaybeTransformer, CompletableTransformer { 14 | 15 | override fun apply(upstream: Observable): ObservableSource = upstream 16 | .subscribeOn(subscribeScheduler) 17 | .observeOn(observeScheduler) 18 | 19 | override fun apply(upstream: Flowable): Publisher = upstream 20 | .subscribeOn(subscribeScheduler) 21 | .observeOn(observeScheduler) 22 | 23 | override fun apply(upstream: Single): SingleSource = upstream 24 | .subscribeOn(subscribeScheduler) 25 | .observeOn(observeScheduler) 26 | 27 | override fun apply(upstream: Maybe): MaybeSource = upstream 28 | .subscribeOn(subscribeScheduler) 29 | .observeOn(observeScheduler) 30 | 31 | override fun apply(upstream: Completable): CompletableSource = upstream 32 | .subscribeOn(subscribeScheduler) 33 | .observeOn(observeScheduler) 34 | } 35 | -------------------------------------------------------------------------------- /retrofit-rxjava3/src/main/java/com/dylanc/retrofit/rxjava3/Transformers.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.rxjava3 4 | 5 | import android.app.Dialog 6 | import androidx.fragment.app.DialogFragment 7 | import androidx.fragment.app.FragmentManager 8 | import com.dylanc.retrofit.rxjava3.RequestLoading.Companion.TAG_LOADING 9 | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers 10 | import io.reactivex.rxjava3.core.Flowable 11 | import io.reactivex.rxjava3.core.Observable 12 | import io.reactivex.rxjava3.core.Single 13 | import io.reactivex.rxjava3.schedulers.Schedulers 14 | import okhttp3.ResponseBody 15 | import java.io.File 16 | 17 | fun Observable.io2mainThread(): Observable = 18 | compose(Transformers.io2mainThread()) 19 | 20 | fun Flowable.io2mainThread(): Flowable = 21 | compose(Transformers.io2mainThread()) 22 | 23 | fun Single.io2mainThread(): Single = 24 | compose(Transformers.io2mainThread()) 25 | 26 | fun Observable.showLoading(requestLoading: RequestLoading): Observable = 27 | compose(Transformers.showLoading(requestLoading)) 28 | 29 | fun Flowable.showLoading(requestLoading: RequestLoading): Flowable = 30 | compose(Transformers.showLoading(requestLoading)) 31 | 32 | fun Single.showLoading(requestLoading: RequestLoading): Single = 33 | compose(Transformers.showLoading(requestLoading)) 34 | 35 | fun Observable.showLoading(onLoading: (Boolean) -> Unit): Observable = 36 | compose(Transformers.showLoading(onLoading)) 37 | 38 | fun Flowable.showLoading(onLoading: (Boolean) -> Unit): Flowable = 39 | compose(Transformers.showLoading(onLoading)) 40 | 41 | fun Single.showLoading(onLoading: (Boolean) -> Unit): Single = 42 | compose(Transformers.showLoading(onLoading)) 43 | 44 | fun Observable.showLoading(fragmentManager: FragmentManager, dialogFragment: DialogFragment): Observable = 45 | compose(Transformers.showLoading(fragmentManager, dialogFragment)) 46 | 47 | fun Flowable.showLoading(fragmentManager: FragmentManager, dialogFragment: DialogFragment): Flowable = 48 | compose(Transformers.showLoading(fragmentManager, dialogFragment)) 49 | 50 | fun Single.showLoading(fragmentManager: FragmentManager, dialogFragment: DialogFragment): Single = 51 | compose(Transformers.showLoading(fragmentManager, dialogFragment)) 52 | 53 | fun Observable.showLoading(dialog: Dialog): Observable = 54 | compose(Transformers.showLoading(dialog)) 55 | 56 | fun Flowable.showLoading(dialog: Dialog): Flowable = 57 | compose(Transformers.showLoading(dialog)) 58 | 59 | fun Single.showLoading(dialog: Dialog): Single = 60 | compose(Transformers.showLoading(dialog)) 61 | 62 | fun Observable.toFile(pathname: String): Observable = 63 | compose(Transformers.toFile(pathname)) 64 | 65 | fun Flowable.toFile(pathname: String): Flowable = 66 | compose(Transformers.toFile(pathname)) 67 | 68 | fun Single.toFile(pathname: String): Single = 69 | compose(Transformers.toFile(pathname)) 70 | 71 | object Transformers { 72 | 73 | @JvmStatic 74 | fun io2mainThread() = 75 | ThreadTransformer(Schedulers.io(), AndroidSchedulers.mainThread()) 76 | 77 | @JvmStatic 78 | fun showLoading(requestLoading: RequestLoading) = 79 | LoadingTransformer(requestLoading) 80 | 81 | @JvmStatic 82 | @JvmOverloads 83 | fun showLoading(fragmentManager: FragmentManager, dialogFragment: DialogFragment, tag: String = TAG_LOADING) = 84 | showLoading(RequestLoading.create(fragmentManager, dialogFragment, tag)) 85 | 86 | @JvmStatic 87 | fun showLoading(dialog: Dialog) = 88 | showLoading(RequestLoading.create(dialog)) 89 | 90 | @JvmStatic 91 | fun toFile(pathname: String): FileTransformer = 92 | FileTransformer(pathname) 93 | } 94 | -------------------------------------------------------------------------------- /retrofit-rxjava3/src/test/java/com/dylanc/retrofit/rxjava3/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.rxjava3 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /sample-java/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample-java/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | android { 6 | compileSdkVersion 30 7 | defaultConfig { 8 | applicationId "com.dylanc.retrofit.sample.java" 9 | minSdkVersion 21 10 | targetSdkVersion 30 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | compileOptions { 22 | sourceCompatibility JavaVersion.VERSION_1_8 23 | targetCompatibility JavaVersion.VERSION_1_8 24 | } 25 | 26 | kotlinOptions { 27 | jvmTarget = JavaVersion.VERSION_1_8.toString() 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation fileTree(dir: 'libs', include: ['*.jar']) 33 | implementation 'androidx.appcompat:appcompat:1.3.1' 34 | implementation 'androidx.constraintlayout:constraintlayout:2.1.0' 35 | implementation 'androidx.activity:activity-ktx:1.3.1' 36 | implementation project(':retrofit-ktx') 37 | implementation project(':retrofit-rxjava2') 38 | implementation 'me.jessyan:progressmanager:1.5.0' 39 | implementation 'top.zibin:Luban:1.1.8' 40 | implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version" 41 | implementation "com.uber.autodispose:autodispose:$autodispose_version" 42 | implementation "com.uber.autodispose:autodispose-android-archcomponents:$autodispose_version" 43 | // implementation "com.uber.autodispose2:autodispose:2.1.1" 44 | // implementation "com.uber.autodispose2:autodispose-android-archcomponents:2.1.1" 45 | 46 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' 47 | testImplementation 'junit:junit:4.13.2' 48 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 49 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 50 | } -------------------------------------------------------------------------------- /sample-java/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /sample-java/src/androidTest/java/com/dylanc/retrofit/sample/java/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.sample.java 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.dylanc.retrofit.sample", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample-java/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/App.java: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.sample.java; 2 | 3 | import android.app.Application; 4 | import android.util.Log; 5 | 6 | import com.dylanc.retrofit.cookiejar.PersistentCookieJarFactory; 7 | import com.dylanc.retrofit.helper.RetrofitHelper; 8 | import com.dylanc.retrofit.sample.java.constant.Constants; 9 | import com.dylanc.retrofit.sample.java.network.GlobalErrorHandler; 10 | 11 | import me.jessyan.progressmanager.ProgressManager; 12 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 13 | import retrofit2.converter.scalars.ScalarsConverterFactory; 14 | 15 | public class App extends Application { 16 | 17 | @Override 18 | public void onCreate() { 19 | super.onCreate(); 20 | RetrofitHelper.getDefault() 21 | .baseUrl(Constants.BASE_URL) 22 | .retryOnConnectionFailure(false) 23 | .putDomain("gank", Constants.URL_GANK) 24 | .multipleDomains() 25 | .printHttpLog(BuildConfig.DEBUG) 26 | .doOnResponse(GlobalErrorHandler::handleResponse) 27 | .persistentCookiesJar(this) 28 | .cookieJar(PersistentCookieJarFactory.create(this)) 29 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 30 | .addConverterFactory(ScalarsConverterFactory.create()) 31 | .okHttpClientBuilder(builder -> { 32 | ProgressManager.getInstance().with(builder); 33 | ProgressManager.getInstance().setRefreshTime(10); 34 | }) 35 | .init(); 36 | } 37 | } -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/api/GankApi.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.sample.java.api 2 | 3 | import com.dylanc.retrofit.interceptor.DomainName 4 | import io.reactivex.Single 5 | import retrofit2.http.GET 6 | 7 | /** 8 | * @author Dylan Cai 9 | */ 10 | interface GankApi { 11 | 12 | @DomainName("gank") 13 | @GET("today") 14 | fun getGankTodayListByRxJava(): Single 15 | } -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/api/RxJavaApi.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.sample.java.api 2 | 3 | import com.dylanc.retrofit.sample.java.bean.ApiResponse 4 | import com.dylanc.retrofit.sample.java.bean.UserBean 5 | import io.reactivex.Single 6 | import retrofit2.http.GET 7 | import retrofit2.http.Path 8 | 9 | /** 10 | * @author Dylan Cai 11 | */ 12 | interface RxJavaApi { 13 | 14 | @GET("/article/list/{page}/json") 15 | fun geArticleList(@Path(value = "page") page: Int): Single 16 | 17 | @GET("/user/login") 18 | fun login(): Single> 19 | } -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/bean/ApiResponse.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.sample.java.bean 2 | 3 | /** 4 | * @author Dylan Cai 5 | * @since 2019/9/28 6 | */ 7 | data class ApiResponse( 8 | val code: Int, 9 | val msg: String, 10 | val data: T 11 | ) -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/bean/UserBean.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.sample.java.bean 2 | 3 | /** 4 | * @author Dylan Cai 5 | * @since 2019/9/28 6 | */ 7 | data class UserBean( 8 | val userId: Int, 9 | val userName: String 10 | ) -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/constant/Constants.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Constants") 2 | 3 | package com.dylanc.retrofit.sample.java.constant 4 | 5 | const val BASE_URL = "https://www.wanandroid.com" 6 | 7 | const val URL_GANK = "https://gank.io/api/" 8 | 9 | const val DOWNLOAD_URL = "https://t7.baidu.com/it/u=3734967019,941734598&fm=193&f=GIF" 10 | -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/network/ApiException.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.sample.java.network 2 | 3 | class ApiException(message: String?) : Exception(message) -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/network/AutoDisposable.java: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.sample.java.network; 2 | 3 | import androidx.lifecycle.Lifecycle; 4 | import androidx.lifecycle.LifecycleOwner; 5 | 6 | import com.uber.autodispose.AutoDispose; 7 | import com.uber.autodispose.AutoDisposeConverter; 8 | import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider; 9 | 10 | /** 11 | * @author Dylan Cai 12 | */ 13 | public class AutoDisposable { 14 | 15 | public static AutoDisposeConverter from(LifecycleOwner lifecycleOwner) { 16 | return AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner)); 17 | } 18 | 19 | public static AutoDisposeConverter from(LifecycleOwner lifecycleOwner, Lifecycle.Event untilEvent) { 20 | return AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner, untilEvent)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/network/DebugInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.sample.java.network 2 | 3 | import android.content.Context 4 | import okhttp3.Interceptor 5 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 6 | import okhttp3.Protocol 7 | import okhttp3.Response 8 | import okhttp3.ResponseBody.Companion.toResponseBody 9 | import java.io.IOException 10 | 11 | /** 12 | * @author Dylan Cai 13 | */ 14 | class DebugInterceptor( 15 | private val context: Context, 16 | private val debugUrl: String, 17 | private val debugRawId: Int 18 | ) : Interceptor { 19 | 20 | @Throws(IOException::class) 21 | override fun intercept(chain: Interceptor.Chain): Response { 22 | val url = chain.request().url.toString() 23 | return if (url.contains(debugUrl)) { 24 | createDebugResponse(chain, debugRawId) 25 | } else { 26 | chain.proceed(chain.request()) 27 | } 28 | } 29 | 30 | private fun createDebugResponse(chain: Interceptor.Chain, rawId: Int): Response { 31 | val inputStream = context.resources.openRawResource(rawId) 32 | val json = inputStream.bufferedReader().use { it.readText() } 33 | return Response.Builder() 34 | .code(200) 35 | .addHeader("Content-Type", "application/json") 36 | .addHeader("Content-Length", json.length.toString()) 37 | .body(json.toResponseBody("application/json".toMediaTypeOrNull())) 38 | .message("OK") 39 | .request(chain.request()) 40 | .protocol(Protocol.HTTP_1_1) 41 | .build() 42 | } 43 | } -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/network/GlobalErrorHandler.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.sample.java.network 2 | 3 | import okhttp3.Response 4 | 5 | /** 6 | * @author Dylan Cai 7 | * @since 2019/10/11 8 | */ 9 | object GlobalErrorHandler { 10 | 11 | @Suppress("UNUSED_PARAMETER") 12 | @JvmStatic 13 | fun handleResponse(response: Response, url: String, body: String): Response { 14 | // var jsonObject: JSONObject? = null 15 | // try { 16 | // jsonObject = JSONObject(body) 17 | // } catch (e: Exception) { 18 | // e.printStackTrace() 19 | // } 20 | // if (jsonObject != null) { 21 | // if (jsonObject.optInt("code", -1) != 200 && jsonObject.has("msg")) { 22 | // throw RuntimeException(jsonObject.getString("msg")) 23 | // } 24 | // } 25 | return response 26 | } 27 | } -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/network/LoadingDialog.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.sample.java.network 2 | 3 | import android.app.Dialog 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AlertDialog 6 | import androidx.fragment.app.DialogFragment 7 | 8 | 9 | class LoadingDialog : DialogFragment() { 10 | 11 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 12 | return AlertDialog.Builder(requireContext()) 13 | .setTitle("loading") 14 | .setMessage("wait a minute...") 15 | .setCancelable(false) 16 | .create() 17 | } 18 | } -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/network/ProgressTransformer.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.retrofit.sample.java.network 4 | 5 | import com.dylanc.retrofit.helper.RetrofitHelper 6 | import io.reactivex.* 7 | import me.jessyan.progressmanager.ProgressListener 8 | import me.jessyan.progressmanager.ProgressManager 9 | import me.jessyan.progressmanager.body.ProgressInfo 10 | import org.reactivestreams.Publisher 11 | 12 | /** 13 | * @author Dylan Cai 14 | */ 15 | 16 | fun RetrofitHelper.Builder.observeProgress() = 17 | okHttpClientBuilder { 18 | ProgressManager.getInstance().with(it) 19 | } 20 | 21 | fun Observable.observeDownload( 22 | url: String, 23 | onProgress: (progressInfo: ProgressInfo) -> Unit, 24 | onError: (id: Long, e: Exception) -> Unit 25 | ): Observable = 26 | compose(ProgressTransformer.observeDownload(url, onProgress, onError)) 27 | 28 | fun Observable.observeDiffDownloadOnSameUrl( 29 | url: String, 30 | key: String? = null, 31 | onProgress: (progressInfo: ProgressInfo) -> Unit, 32 | onError: (id: Long, e: Exception) -> Unit 33 | ): Observable = 34 | compose(ProgressTransformer.observeDiffDownloadOnSameUrl(url, key, onProgress, onError)) 35 | 36 | fun Observable.observeUpload( 37 | url: String, 38 | onProgress: (progressInfo: ProgressInfo) -> Unit, 39 | onError: (id: Long, e: Exception) -> Unit 40 | ): Observable = 41 | compose(ProgressTransformer.observeUpload(url, onProgress, onError)) 42 | 43 | fun Observable.observeDiffUploadOnSameUrl( 44 | url: String, 45 | key: String? = null, 46 | onProgress: (progressInfo: ProgressInfo) -> Unit, 47 | onError: (id: Long, e: Exception) -> Unit 48 | ): Observable = 49 | compose(ProgressTransformer.observeDiffUploadOnSameUrl(url, key, onProgress, onError)) 50 | 51 | fun Flowable.observeDownload( 52 | url: String, 53 | onProgress: (progressInfo: ProgressInfo) -> Unit, 54 | onError: (id: Long, e: Exception) -> Unit 55 | ): Flowable = 56 | compose(ProgressTransformer.observeDownload(url, onProgress, onError)) 57 | 58 | fun Flowable.observeDiffDownloadOnSameUrl( 59 | url: String, 60 | key: String? = null, 61 | onProgress: (progressInfo: ProgressInfo) -> Unit, 62 | onError: (id: Long, e: Exception) -> Unit 63 | ): Flowable = 64 | compose(ProgressTransformer.observeDiffDownloadOnSameUrl(url, key, onProgress, onError)) 65 | 66 | fun Flowable.observeUpload( 67 | url: String, 68 | onProgress: (progressInfo: ProgressInfo) -> Unit, 69 | onError: (id: Long, e: Exception) -> Unit 70 | ): Flowable = 71 | compose(ProgressTransformer.observeUpload(url, onProgress, onError)) 72 | 73 | fun Flowable.observeDiffUploadOnSameUrl( 74 | url: String, 75 | key: String? = null, 76 | onProgress: (progressInfo: ProgressInfo) -> Unit, 77 | onError: (id: Long, e: Exception) -> Unit 78 | ): Flowable = 79 | compose(ProgressTransformer.observeDiffUploadOnSameUrl(url, key, onProgress, onError)) 80 | 81 | fun Single.observeDownload( 82 | url: String, 83 | onProgress: (progressInfo: ProgressInfo) -> Unit, 84 | onError: (id: Long, e: Exception) -> Unit 85 | ): Single = 86 | compose(ProgressTransformer.observeDownload(url, onProgress, onError)) 87 | 88 | fun Single.observeDiffDownloadOnSameUrl( 89 | url: String, 90 | key: String? = null, 91 | onProgress: (progressInfo: ProgressInfo) -> Unit, 92 | onError: (id: Long, e: Exception) -> Unit 93 | ): Single = 94 | compose(ProgressTransformer.observeDiffDownloadOnSameUrl(url, key, onProgress, onError)) 95 | 96 | fun Single.observeUpload( 97 | url: String, 98 | onProgress: (progressInfo: ProgressInfo) -> Unit, 99 | onError: (id: Long, e: Exception) -> Unit 100 | ): Single = 101 | compose(ProgressTransformer.observeUpload(url, onProgress, onError)) 102 | 103 | fun Single.observeDiffUploadOnSameUrl( 104 | url: String, 105 | key: String? = null, 106 | onProgress: (progressInfo: ProgressInfo) -> Unit, 107 | onError: (id: Long, e: Exception) -> Unit 108 | ): Single = 109 | compose(ProgressTransformer.observeDiffUploadOnSameUrl(url, key, onProgress, onError)) 110 | 111 | class ProgressTransformer( 112 | private val onProgress: (progressInfo: ProgressInfo) -> Unit, 113 | private val onError: (id: Long, e: Exception) -> Unit, 114 | private val observer: (listener: ProgressListener) -> Unit 115 | ) : ObservableTransformer, FlowableTransformer, SingleTransformer, 116 | MaybeTransformer, CompletableTransformer { 117 | 118 | companion object { 119 | @JvmStatic 120 | fun observeDownload( 121 | url: String, 122 | onProgress: (progressInfo: ProgressInfo) -> Unit, 123 | onError: (id: Long, e: Exception) -> Unit 124 | ) = ProgressTransformer(onProgress, onError) { listener -> 125 | ProgressManager.getInstance().addResponseListener(url, listener) 126 | } 127 | 128 | @JvmStatic 129 | fun observeDiffDownloadOnSameUrl( 130 | url: String, 131 | key: String? = null, 132 | onProgress: (progressInfo: ProgressInfo) -> Unit, 133 | onError: (id: Long, e: Exception) -> Unit 134 | ) = ProgressTransformer(onProgress, onError) { listener -> 135 | if (key == null) { 136 | ProgressManager.getInstance().addDiffResponseListenerOnSameUrl(url, listener) 137 | } else { 138 | ProgressManager.getInstance().addDiffResponseListenerOnSameUrl(url, key, listener) 139 | } 140 | } 141 | 142 | @JvmStatic 143 | fun observeUpload( 144 | url: String, 145 | onProgress: (progressInfo: ProgressInfo) -> Unit, 146 | onError: (id: Long, e: Exception) -> Unit 147 | ) = ProgressTransformer(onProgress, onError) { listener -> 148 | ProgressManager.getInstance().addRequestListener(url, listener) 149 | } 150 | 151 | @JvmStatic 152 | fun observeDiffUploadOnSameUrl( 153 | url: String, 154 | key: String? = null, 155 | onProgress: (progressInfo: ProgressInfo) -> Unit, 156 | onError: (id: Long, e: Exception) -> Unit 157 | ) = ProgressTransformer(onProgress, onError) { listener -> 158 | if (key == null) { 159 | ProgressManager.getInstance().addDiffRequestListenerOnSameUrl(url, listener) 160 | } else { 161 | ProgressManager.getInstance().addDiffRequestListenerOnSameUrl(url, key, listener) 162 | } 163 | } 164 | } 165 | 166 | override fun apply(upstream: Observable): ObservableSource = upstream 167 | .doOnSubscribe { observer.invoke(listener) } 168 | 169 | override fun apply(upstream: Flowable): Publisher = upstream 170 | .doOnSubscribe { observer.invoke(listener) } 171 | 172 | override fun apply(upstream: Single): SingleSource = upstream 173 | .doOnSubscribe { observer.invoke(listener) } 174 | 175 | override fun apply(upstream: Maybe): MaybeSource = upstream 176 | .doOnSubscribe { observer.invoke(listener) } 177 | 178 | override fun apply(upstream: Completable): CompletableSource = upstream 179 | .doOnSubscribe { observer.invoke(listener) } 180 | 181 | private val listener = object : ProgressListener { 182 | 183 | override fun onProgress(progressInfo: ProgressInfo) { 184 | onProgress.invoke(progressInfo) 185 | } 186 | 187 | override fun onError(id: Long, e: Exception) { 188 | onError.invoke(id, e) 189 | } 190 | } 191 | } -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/network/SslSocketUtils.java: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.sample.java.network; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.security.KeyManagementException; 6 | import java.security.KeyStore; 7 | import java.security.NoSuchAlgorithmException; 8 | import java.security.cert.Certificate; 9 | import java.security.cert.CertificateException; 10 | import java.security.cert.CertificateFactory; 11 | import java.security.cert.X509Certificate; 12 | 13 | import javax.net.ssl.HostnameVerifier; 14 | import javax.net.ssl.KeyManager; 15 | import javax.net.ssl.KeyManagerFactory; 16 | import javax.net.ssl.SSLContext; 17 | import javax.net.ssl.SSLSession; 18 | import javax.net.ssl.SSLSocketFactory; 19 | import javax.net.ssl.TrustManager; 20 | import javax.net.ssl.TrustManagerFactory; 21 | import javax.net.ssl.X509TrustManager; 22 | 23 | public class SslSocketUtils { 24 | 25 | public static class SSLParams { 26 | public SSLSocketFactory sSLSocketFactory; 27 | public X509TrustManager trustManager; 28 | } 29 | 30 | public static SSLParams getSslSocketFactory() { 31 | return getSslSocketFactoryBase(null, null, null); 32 | } 33 | 34 | /** 35 | * https单向认证 36 | * 可以额外配置信任服务端的证书策略,否则默认是按CA证书去验证的,若不是CA可信任的证书,则无法通过验证 37 | */ 38 | public static SSLParams getSslSocketFactory(X509TrustManager trustManager) { 39 | return getSslSocketFactoryBase(trustManager, null, null); 40 | } 41 | 42 | /** 43 | * https单向认证 44 | * 用含有服务端公钥的证书校验服务端证书 45 | */ 46 | public static SSLParams getSslSocketFactory(InputStream... certificates) { 47 | return getSslSocketFactoryBase(null, null, null, certificates); 48 | } 49 | 50 | /** 51 | * https双向认证 52 | * bksFile 和 password -> 客户端使用bks证书校验服务端证书 53 | * certificates -> 用含有服务端公钥的证书校验服务端证书 54 | */ 55 | public static SSLParams getSslSocketFactory(InputStream bksFile, String password, InputStream... certificates) { 56 | return getSslSocketFactoryBase(null, bksFile, password, certificates); 57 | } 58 | 59 | /** 60 | * https双向认证 61 | * bksFile 和 password -> 客户端使用bks证书校验服务端证书 62 | * X509TrustManager -> 如果需要自己校验,那么可以自己实现相关校验,如果不需要自己校验,那么传null即可 63 | */ 64 | public static SSLParams getSslSocketFactory(InputStream bksFile, String password, X509TrustManager trustManager) { 65 | return getSslSocketFactoryBase(trustManager, bksFile, password); 66 | } 67 | 68 | private static SSLParams getSslSocketFactoryBase(X509TrustManager trustManager, InputStream bksFile, String password, InputStream... certificates) { 69 | SSLParams sslParams = new SSLParams(); 70 | try { 71 | KeyManager[] keyManagers = prepareKeyManager(bksFile, password); 72 | TrustManager[] trustManagers = prepareTrustManager(certificates); 73 | X509TrustManager manager; 74 | if (trustManager != null) { 75 | //优先使用用户自定义的TrustManager 76 | manager = trustManager; 77 | } else if (trustManagers != null) { 78 | //然后使用默认的TrustManager 79 | manager = chooseTrustManager(trustManagers); 80 | } else { 81 | //否则使用不安全的TrustManager 82 | manager = UnSafeTrustManager; 83 | } 84 | // 创建TLS类型的SSLContext对象, that uses our TrustManager 85 | SSLContext sslContext = SSLContext.getInstance("SSL"); 86 | // 用上面得到的trustManagers初始化SSLContext,这样sslContext就会信任keyStore中的证书 87 | // 第一个参数是授权的密钥管理器,用来授权验证,比如授权自签名的证书验证。第二个是被授权的证书管理器,用来验证服务器端的证书 88 | sslContext.init(keyManagers, new TrustManager[]{manager}, null); 89 | // 通过sslContext获取SSLSocketFactory对象 90 | sslParams.sSLSocketFactory = sslContext.getSocketFactory(); 91 | sslParams.trustManager = manager; 92 | return sslParams; 93 | } catch (NoSuchAlgorithmException e) { 94 | throw new AssertionError(e); 95 | } catch (KeyManagementException e) { 96 | throw new AssertionError(e); 97 | } 98 | } 99 | 100 | private static KeyManager[] prepareKeyManager(InputStream bksFile, String password) { 101 | try { 102 | if (bksFile == null || password == null) return null; 103 | KeyStore clientKeyStore = KeyStore.getInstance("BKS"); 104 | clientKeyStore.load(bksFile, password.toCharArray()); 105 | KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 106 | kmf.init(clientKeyStore, password.toCharArray()); 107 | return kmf.getKeyManagers(); 108 | } catch (Exception e) { 109 | 110 | } 111 | return null; 112 | } 113 | 114 | private static TrustManager[] prepareTrustManager(InputStream... certificates) { 115 | if (certificates == null || certificates.length <= 0) return null; 116 | try { 117 | CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); 118 | // 创建一个默认类型的KeyStore,存储我们信任的证书 119 | KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 120 | keyStore.load(null); 121 | int index = 0; 122 | for (InputStream certStream : certificates) { 123 | String certificateAlias = Integer.toString(index++); 124 | // 证书工厂根据证书文件的流生成证书 cert 125 | Certificate cert = certificateFactory.generateCertificate(certStream); 126 | // 将 cert 作为可信证书放入到keyStore中 127 | keyStore.setCertificateEntry(certificateAlias, cert); 128 | try { 129 | if (certStream != null) certStream.close(); 130 | } catch (IOException e) { 131 | 132 | } 133 | } 134 | //我们创建一个默认类型的TrustManagerFactory 135 | TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 136 | //用我们之前的keyStore实例初始化TrustManagerFactory,这样tmf就会信任keyStore中的证书 137 | tmf.init(keyStore); 138 | //通过tmf获取TrustManager数组,TrustManager也会信任keyStore中的证书 139 | return tmf.getTrustManagers(); 140 | } catch (Exception e) { 141 | 142 | } 143 | return null; 144 | } 145 | 146 | private static X509TrustManager chooseTrustManager(TrustManager[] trustManagers) { 147 | for (TrustManager trustManager : trustManagers) { 148 | if (trustManager instanceof X509TrustManager) { 149 | return (X509TrustManager) trustManager; 150 | } 151 | } 152 | return null; 153 | } 154 | 155 | /** 156 | * 为了解决客户端不信任服务器数字证书的问题,网络上大部分的解决方案都是让客户端不对证书做任何检查, 157 | * 这是一种有很大安全漏洞的办法 158 | */ 159 | public static X509TrustManager UnSafeTrustManager = new X509TrustManager() { 160 | @Override 161 | public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { 162 | } 163 | 164 | @Override 165 | public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 166 | } 167 | 168 | @Override 169 | public X509Certificate[] getAcceptedIssuers() { 170 | return new X509Certificate[]{}; 171 | } 172 | }; 173 | 174 | /** 175 | * 此类是用于主机名验证的基接口。 在握手期间,如果 URL 的主机名和服务器的标识主机名不匹配, 176 | * 则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。策略可以是基于证书的或依赖于其他验证方案。 177 | * 当验证 URL 主机名使用的默认规则失败时使用这些回调。如果主机名是可接受的,则返回 true 178 | */ 179 | public static HostnameVerifier UnSafeHostnameVerifier = new HostnameVerifier() { 180 | @Override 181 | public boolean verify(String hostname, SSLSession session) { 182 | return true; 183 | } 184 | }; 185 | } -------------------------------------------------------------------------------- /sample-java/src/main/java/com/dylanc/retrofit/sample/java/ui/JavaActivity.java: -------------------------------------------------------------------------------- 1 | package com.dylanc.retrofit.sample.java.ui; 2 | 3 | import android.os.Bundle; 4 | import android.view.View; 5 | import android.widget.Toast; 6 | 7 | import androidx.appcompat.app.AlertDialog; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | 10 | import com.dylanc.retrofit.helper.RetrofitHelper; 11 | import com.dylanc.retrofit.rxjava2.RxDownloadApi; 12 | import com.dylanc.retrofit.rxjava2.Transformers; 13 | import com.dylanc.retrofit.sample.java.R; 14 | import com.dylanc.retrofit.sample.java.api.GankApi; 15 | import com.dylanc.retrofit.sample.java.api.RxJavaApi; 16 | import com.dylanc.retrofit.sample.java.constant.Constants; 17 | import com.dylanc.retrofit.sample.java.network.LoadingDialog; 18 | import com.uber.autodispose.AutoDispose; 19 | import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider; 20 | 21 | import java.util.Objects; 22 | 23 | public class JavaActivity extends AppCompatActivity { 24 | 25 | private final LoadingDialog loadingDialog = new LoadingDialog(); 26 | 27 | @Override 28 | protected void onCreate(Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | setContentView(R.layout.activity_sample); 31 | } 32 | 33 | public void requestArticleList(View view) { 34 | RetrofitHelper.create(RxJavaApi.class) 35 | .geArticleList(0) 36 | .compose(Transformers.io2mainThread()) 37 | .compose(Transformers.showLoading(getSupportFragmentManager(), loadingDialog)) 38 | .as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this))) 39 | .subscribe( 40 | this::alert, 41 | e -> toast(e.getMessage()) 42 | ); 43 | } 44 | 45 | public void requestGankTodayList(View view) { 46 | RetrofitHelper.create(GankApi.class) 47 | .getGankTodayListByRxJava() 48 | .compose(Transformers.io2mainThread()) 49 | .compose(Transformers.showLoading(getSupportFragmentManager(), loadingDialog)) 50 | .as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this))) 51 | .subscribe( 52 | this::alert, 53 | e -> toast(e.getMessage()) 54 | ); 55 | } 56 | 57 | public void requestLogin(View view) { 58 | RetrofitHelper.create(RxJavaApi.class) 59 | .login() 60 | .compose(Transformers.io2mainThread()) 61 | .compose(Transformers.showLoading(getSupportFragmentManager(), loadingDialog)) 62 | .as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this))) 63 | .subscribe( 64 | response -> toast("登录成功"), 65 | e -> toast(e.getMessage()) 66 | ); 67 | } 68 | 69 | public void download(View view) { 70 | final String pathname = Objects.requireNonNull(getExternalCacheDir()).getPath() + "/test.png"; 71 | RetrofitHelper.create(RxDownloadApi.class) 72 | .download(Constants.DOWNLOAD_URL) 73 | .compose(Transformers.toFile(pathname)) 74 | .compose(Transformers.io2mainThread()) 75 | .compose(Transformers.showLoading(getSupportFragmentManager(), loadingDialog)) 76 | .as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this))) 77 | .subscribe( 78 | file -> toast("已下载到" + file.getPath()), 79 | e -> toast(e.getMessage()) 80 | ); 81 | } 82 | 83 | private void alert(String msg) { 84 | new AlertDialog.Builder(this) 85 | .setTitle("Response data") 86 | .setMessage(msg) 87 | .create() 88 | .show(); 89 | } 90 | 91 | private void toast(String msg) { 92 | Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /sample-java/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /sample-java/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /sample-java/src/main/res/layout/activity_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 |