diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..9466725 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ec4bb38 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_zh_template_bug.yml b/.github/ISSUE_TEMPLATE/issue_zh_template_bug.yml new file mode 100644 index 0000000..907aab2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_zh_template_bug.yml @@ -0,0 +1,173 @@ +name: 提交 Bug +description: 请告诉我框架存在的问题,我会协助你解决此问题! +title: "[Bug]:" +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) + - type: input + id: input_id_1 + attributes: + label: 框架版本【必填】 + description: 请输入你使用的框架版本 + validations: + required: true + - type: textarea + id: input_id_2 + attributes: + label: 问题描述【必填】 + description: 请输入你对这个问题的描述 + validations: + required: true + - type: textarea + id: input_id_3 + attributes: + label: 复现步骤【必填】 + description: 请输入问题的复现步骤 + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: 是否必现【必填】 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true + - type: input + id: input_id_5 + attributes: + label: 项目 targetSdkVersion【必填】 + validations: + required: true + - type: input + id: input_id_6 + attributes: + label: 出现问题的手机信息【必填】 + description: 请填写出现问题的品牌和机型 + validations: + required: true + - type: input + id: input_id_7 + attributes: + label: 出现问题的安卓版本【必填】 + description: 请填写出现问题的 Android 版本 + validations: + required: true + - type: dropdown + id: input_id_8 + attributes: + label: 问题信息的来源渠道【必填】 + multiple: true + options: + - 自己遇到的 + - Bugly 看到的 + - 用户反馈 + - 其他渠道 + - type: input + id: input_id_9 + attributes: + label: 是部分机型还是所有机型都会出现【必答】 + description: 部分/全部(例如:某为,某 Android 版本会出现) + validations: + required: true + - type: dropdown + id: input_id_10 + attributes: + label: 框架最新的版本是否存在这个问题【必答】 + description: 如果用的是旧版本的话,建议升级看问题是否还存在 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true + - type: dropdown + id: input_id_11 + attributes: + label: 框架文档是否提及了该问题【必答】 + description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true + - type: dropdown + id: input_id_12 + attributes: + label: 是否已经查阅框架文档但还未能解决的【必答】 + description: 如果查阅了文档但还是没有解决的话,可以选择是 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true + - type: dropdown + id: input_id_13 + attributes: + label: issue 列表中是否有人曾提过类似的问题【必答】 + description: 可以在 issue 列表在搜索问题关键字,参考一下别人的解决方案 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true + - type: dropdown + id: input_id_14 + attributes: + label: 是否已经搜索过了 issue 列表但还未能解决的【必答】 + description: 如果搜索过了 issue 列表但是问题没有解决的话,可以选择是 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true + - type: dropdown + id: input_id_15 + attributes: + label: 是否可以通过 Demo 来复现该问题【必答】 + description: 排查一下是不是自己的项目代码写得有问题导致的 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true + - type: textarea + id: input_id_16 + attributes: + label: 提供报错堆栈 + description: 如果有报错的话必填,注意不要拿被混淆过的代码堆栈上来 + render: text + validations: + required: false + - type: textarea + id: input_id_17 + attributes: + label: 提供截图或视频 + description: 根据需要提供,此项不强制 + validations: + required: false + - type: textarea + id: input_id_18 + attributes: + label: 提供解决方案 + description: 如果已经解决了的话,此项不强制 + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_zh_template_question.yml b/.github/ISSUE_TEMPLATE/issue_zh_template_question.yml new file mode 100644 index 0000000..b4159ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_zh_template_question.yml @@ -0,0 +1,65 @@ +name: 提出疑问 +description: 提出你的困惑,我会给你解答 +title: "[疑惑]:" +labels: ["question"] + +body: + - type: markdown + attributes: + value: | + ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) + - type: textarea + id: input_id_1 + attributes: + label: 问题描述【必填】 + description: 请描述一下你的问题(注意:如果确定是框架 bug 请不要在这里提,否则一概不受理) + validations: + required: true + - type: dropdown + id: input_id_2 + attributes: + label: 框架文档是否提及了该问题【必答】 + description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true + - type: dropdown + id: input_id_3 + attributes: + label: 是否已经查阅框架文档但还未能解决的【必答】 + description: 如果查阅了文档但还是没有解决的话,可以选择是 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: issue 列表中是否有人曾提过类似的问题【必答】 + description: 可以在 issue 列表在搜索问题关键字,参考一下别人的解决方案 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true + - type: dropdown + id: input_id_5 + attributes: + label: 是否已经搜索过了 issue 列表但还未能解决的【必答】 + description: 如果搜索过了 issue 列表但是问题没有解决的话,可以选择是 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_zh_template_suggest.yml b/.github/ISSUE_TEMPLATE/issue_zh_template_suggest.yml new file mode 100644 index 0000000..f5fea21 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_zh_template_suggest.yml @@ -0,0 +1,60 @@ +name: 提交建议 +description: 请告诉我框架的不足之处,让我做得更好! +title: "[建议]:" +labels: ["help wanted"] + +body: + - type: markdown + attributes: + value: | + ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) + - type: textarea + id: input_id_1 + attributes: + label: 你觉得框架有什么不足之处?【必答】 + description: 你可以描述框架有什么令你不满意的地方 + validations: + required: true + - type: dropdown + id: input_id_2 + attributes: + label: issue 是否有人曾提过类似的建议?【必答】 + description: 一旦出现重复提问我将不会再次解答 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true + - type: dropdown + id: input_id_3 + attributes: + label: 框架文档是否提及了该问题【必答】 + description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: 是否已经查阅框架文档但还未能解决的【必答】 + description: 如果查阅了文档但还是没有解决的话,可以选择是 + multiple: false + options: + - 未选择 + - 是 + - 否 + validations: + required: true + - type: textarea + id: input_id_5 + attributes: + label: 你觉得该怎么去完善会比较好?【非必答】 + description: 你可以提供一下自己的想法或者做法供作者参考 + validations: + required: false \ No newline at end of file diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..635d695 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,15 @@ +name: Android CI + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 diff --git a/EasyHttp.apk b/EasyHttp.apk deleted file mode 100644 index 4db5584..0000000 Binary files a/EasyHttp.apk and /dev/null differ diff --git a/HelpDoc-en.md b/HelpDoc-en.md new file mode 100644 index 0000000..e27e652 --- /dev/null +++ b/HelpDoc-en.md @@ -0,0 +1,1948 @@ +# Table of Contents + +* [Integration Documentation](#integration-documentation) + + * [Configure Permissions](#configure-permissions) + + * [HTTP Plaintext Requests](#http-plaintext-requests) + + * [Server Configuration](#server-configuration) + + * [Framework Initialization](#framework-initialization) + +* [Usage Documentation](#usage-documentation) + + * [Configure API](#configure-api) + + * [Make a Request](#make-a-request) + + * [Upload Files](#upload-files) + + * [Download Files](#download-files) + + * [Scoped Storage Adaptation](#scoped-storage-adaptation) + + * [Make Synchronous Requests](#make-synchronous-requests) + + * [Set Request Cache](#set-request-cache) + + * [Use with Coroutines](#use-with-coroutines) + +* [FAQ](#faq) + + * [How to Set Cookies](#how-to-set-cookies) + + * [How to Add or Remove Global Parameters](#how-to-add-or-remove-global-parameters) + + * [How to Dynamically Add Global Parameters or Headers](#how-to-dynamically-add-global-parameters-or-headers) + + * [How to Ignore a Global Parameter in a Request](#how-to-ignore-a-global-parameter-in-a-request) + + * [How to Get Server Configuration](#how-to-get-server-configuration) + + * [How to Modify API Server Configuration](#how-to-modify-api-server-configuration) + + * [How to Configure Multiple Domains](#how-to-configure-multiple-domains) + + * [How to Change Parameter Submission Method](#how-to-change-parameter-submission-method) + + * [How to Encrypt or Decrypt API](#how-to-encrypt-or-decrypt-api) + + * [How to Ignore a Parameter](#how-to-ignore-a-parameter) + + * [How to Pass Request Headers](#how-to-pass-request-headers) + + * [How to Pass Dynamic Request Headers](#how-to-pass-dynamic-request-headers) + + * [How to Rename Parameter or Header Name](#how-to-rename-parameter-or-header-name) + + * [How to Upload Files](#how-to-upload-files) + + * [How to Upload a List of Files](#how-to-upload-a-list-of-files) + + * [How to Set Timeout Retry](#how-to-set-timeout-retry) + + * [How to Set Request Timeout](#how-to-set-request-timeout) + + * [How to Disable Log Printing](#how-to-disable-log-printing) + + * [How to Change Log Printing Strategy](#how-to-change-log-printing-strategy) + + * [How to Cancel an Ongoing Request](#how-to-cancel-an-ongoing-request) + + * [How to Delay a Request](#how-to-delay-a-request) + + * [How to Dynamically Concatenate API Path](#how-to-dynamically-concatenate-api-path) + + * [How to Dynamically Set the Entire Request URL](#how-to-dynamically-set-the-entire-request-url) + + * [How to Trust All Certificates for HTTPS](#how-to-trust-all-certificates-for-https) + + * [What if I Don't Want to Write a Class for Each API](#what-if-i-dont-want-to-write-a-class-for-each-api) + + * [What if the Framework Only Accepts LifecycleOwner](#what-if-the-framework-only-accepts-lifecycleowner) + + * [How to Use EasyHttp in ViewModel](#how-to-use-easyhttp-in-viewmodel) + + * [How to Cancel the Loading Dialog When Cancelling a Request](#how-to-cancel-the-loading-dialog-when-cancelling-a-request) + + * [How to Upload a JSON Array as a Parameter](#how-to-upload-a-json-array-as-a-parameter) + + * [What if the Key of API Parameter is Dynamic](#what-if-the-key-of-api-parameter-is-dynamic) + + * [How to Set a Custom UA Identifier](#how-to-set-a-custom-ua-identifier) + + * [How to Change the Thread for Request Callback](#how-to-change-the-thread-for-request-callback) + + * [How to Use a Custom RequestBody for Requests](#how-to-use-a-custom-requestbody-for-requests) + + * [How to Customize ContentType in Request Header](#how-to-customize-contenttype-in-request-header) + + * [How to Customize Key and Value in Get Request Parameters](#how-to-customize-key-and-value-in-get-request-parameters) + + * [How to Define Get-like Parameters in Post Request](#how-to-define-get-like-parameters-in-post-request) + +* [Use with RxJava](#use-with-rxjava) + + * [Preparation](#preparation) + + * [Multiple Requests in Series](#multiple-requests-in-series) + + * [Polling Requests](#polling-requests) + + * [Wrap Returned Data](#wrap-returned-data) + +* [Support Protobuf](#support-protobuf) + + * [Preparation](#preparation-1) + + * [Parse Request Body as Protobuf](#parse-request-body-as-protobuf) + + * [Parse Response Body as Protobuf](#parse-response-body-as-protobuf) + +# Integration Documentation + +#### Configure Permissions + +```xml + + + + + + +``` + +#### HTTP Plaintext Requests + +* **Android 9.0** restricts plaintext traffic network requests, non-encrypted traffic requests will be prohibited by the system. + +* If the current application's request is an http request, but not https, this will lead to the system prohibiting the current application from performing this request, if the WebView's url uses the http protocol, it will also fail to load, https is unaffected + +* Create a new xml directory in res, then create a file named: `network_security_config.xml`, the content of this file is as follows + +```xml + + + + +``` + +* Then apply the above xml configuration in the application tag of AndroidManifest.xml + +```xml + +``` + +#### Server Configuration + +```java +public class RequestServer implements IRequestServer { + + @NonNull + @Override + public String getHost() { + return "https://www.baidu.com/"; + } + + @NonNull + @Override + public IHttpPostBodyStrategy getBodyType() { + // Parameters submitted in Json format (default is form) + return RequestBodyType.JSON; + } +} +``` + +#### Framework Initialization + +* Need to configure request result handling, specific encapsulation can refer to [RequestHandler](app/src/main/java/com/hjq/easy/demo/http/model/RequestHandler.java) + +```java +OkHttpClient okHttpClient = new OkHttpClient.Builder() + .build(); + +EasyConfig.with(okHttpClient) + // Whether to print logs + .setLogEnabled(BuildConfig.DEBUG) + // Set server configuration (must be set) + .setServer(server) + // Set request processing strategy (must be set) + .setHandler(new RequestHandler()) + // Set request cache implementation strategy (not required) + //.setCacheStrategy(new HttpCacheStrategy()) + // Set request retry count + .setRetryCount(3) + // Add global request parameters + //.addParam("token", "6666666") + // Add global request headers + //.addHeader("time", "20191030") + // Enable configuration + .into(); +``` + +* This is for creating configuration, updating configuration can use + +```java +EasyConfig.getInstance() + .addParam("token", data.getData().getToken()); +``` + +# Usage Documentation + +#### Configure API + +```java +public final class LoginApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "user/login"; + } + + /** Username */ + private String userName; + + /** Login password */ + private String password; + + public LoginApi setUserName(String userName) { + this.userName = userName; + return this; + } + + public LoginApi setPassword(String password) { + this.password = password; + return this; + } +} +``` + +* You can add some annotations to the fields of this class + + * @HttpHeader: Mark this field as a request header parameter + + * @HttpIgnore: Mark this field will not be sent to the backend + + * @HttpRename: Redefine the parameter name or header name sent to the backend + +* You can implement some interfaces in this class + + * implements IRequestHost: After implementing this interface, you can re-specify the main host address of this request + + * implements IRequestBodyType: After implementing this interface, you can re-specify the submission method of this request body + + * implements IRequestCacheConfig: After implementing this interface, you can re-specify the cache mode configuration of this request + + * implements IRequestHttpClient: After implementing this interface, you can re-specify the OkHttpClient object used for this request + +* The field is the standard for measuring request parameters + + * Assuming the attribute value of a field is empty, this field will not be sent to the backend as a request parameter + + * Assuming a field type is String, its attribute value is an empty string, then this field will be sent as a request parameter, if it is an empty object, it will not + + * Assuming a field type is int, since basic data types do not have empty values, this field will definitely be sent as a request parameter, but it can be replaced with an Integer object to avoid, because the default value of Integer is null + +* I'll give you an example: [https://www.baidu.com/api/user/getInfo](https://www.baidu.com/),then the standard way to write is + +```java +public final class XxxApi implements IRequestServer, IRequestApi { + + @NonNull + @Override + public String getHost() { + return "https://www.baidu.com/"; + } + + @NonNull + @Override + public String getApi() { + return "user/getInfo"; + } +} +``` + +#### Make a Request + +* Need to configure request status and lifecycle handling, specific encapsulation can refer to [BaseActivity](app/src/main/java/com/hjq/easy/demo/BaseActivity.java) + +```java +EasyHttp.post(this) + .api(new LoginApi() + .setUserName("Android 轮子哥") + .setPassword("123456")) + .request(new HttpCallbackProxy>(activity) { + + @Override + public void onHttpSuccess(@NonNull HttpData data) { + toast("Login successful"); + } + }); +``` + +* This shows the post method, EasyHttp also supports get, head, delete, put, patch requests, which are not demonstrated here + +#### Upload Files + +```java +public final class UpdateImageApi implements IRequestApi, IRequestBodyType { + + @NonNull + @Override + public String getApi() { + return "upload/"; + } + + @NonNull + @Override + public IHttpPostBodyStrategy getBodyType() { + // File upload needs to use the form method + return RequestBodyType.FORM; + } + + /** Local image */ + private File image; + + public UpdateImageApi(File image) { + this.image = image; + } + + public UpdateImageApi setImage(File image) { + this.image = image; + return this; + } +} +``` + +```java +EasyHttp.post(this) + .api(new UpdateImageApi(file)) + .request(new OnUpdateListener() { + + @Override + public void onUpdateStart(@NonNull IRequestApi api) { + mProgressBar.setVisibility(View.VISIBLE); + } + + @Override + public void onUpdateProgressChange(int progress) { + mProgressBar.setProgress(progress); + } + + @Override + public void onUpdateSuccess(@NonNull Void result) { + toast("Upload successful"); + } + + @Override + public void onUpdateFail(@NonNull Throwable throwable) { + toast("Upload failed"); + } + + @Override + public void onUpdateEnd(@NonNull IRequestApi api) { + mProgressBar.setVisibility(View.GONE); + } + }); +``` + +* **Note: If the uploaded file is too large or too many, it may cause a timeout, you can re-set the timeout for this request, the timeout is recommended to be based on the file size, please refer to the documentation for specific timeout settings, you can search directly on this page.** + +* Of course, in addition to using `File` objects for upload, you can also use `FileContentResolver`, `InputStream`, `RequestBody`, `MultipartBody.Part` objects for upload, if you need to batch upload, please use `List`, `List`, `List`, `List`, `List` objects for batch upload. + +#### Download Files + +* Download cache strategy: When the md5 of the specified downloaded file or the backend returns md5, the download framework enables the download cache mode by default. If the file already exists in the phone and the md5 verifies the file integrity, the framework will not download it again, but will directly call the download listener. Reduce server pressure and user waiting time. + +```java +EasyHttp.download(this) + .method(HttpMethod.GET) + .file(new File(Environment.getExternalStorageDirectory(), "微信.apk")) + //.url("https://qd.myapp.com/myapp/qqteam/AndroidQQ/mobileqq_android.apk") + .url("http://dldir1.qq.com/weixin/android/weixin708android1540.apk") + .md5("2E8BDD7686474A7BC4A51ADC3667CABF") + // Set resumable transfer (default is not enabled) + //.resumableTransfer(true) + .listener(new OnDownloadListener() { + + @Override + public void onDownloadStart(@NonNull File file) { + mProgressBar.setVisibility(View.VISIBLE); + } + + @Override + public void onDownloadProgressChange(@NonNull File file, int progress) { + mProgressBar.setProgress(progress); + } + + @Override + public void onDownloadSuccess(@NonNull File file) { + toast("Download complete: " + file.getPath()); + installApk(XxxActivity.this, file); + } + + @Override + public void onDownloadFail(@NonNull File file, @NonNull Throwable throwable) { + toast("Download failed: " + throwable.getMessage()); + file.delete(); + } + + @Override + public void onDownloadEnd(@NonNull File file) { + mProgressBar.setVisibility(View.GONE); + } + + }).start(); +``` + +#### Scoped Storage Adaptation + +* Before Android 10, when reading and writing external storage, we could directly use File objects to upload or download files, but if your project needs the feature of Android 10 scoped storage, then when reading and writing external storage files, we cannot directly use File objects, because `ContentResolver.insert` returns a `Uri` object, at this time, we need to use the `FileContentResolver` object provided by the framework (this object is a subclass of File), the specific usage example is as follows: + +```java +File outputFile; +if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.Q) { + ContentValues values = new ContentValues(); + ......... + // Generate a new uri path + Uri outputUri = getContentResolver().insert(MediaStore.Xxx.Media.EXTERNAL_CONTENT_URI, values); + // Adapt Android 10 scoped storage feature + outputFile = new FileContentResolver(context, outputUri); +} else { + outputFile = new File(xxxx); +} + +EasyHttp.post(this) + .api(new XxxApi() + .setImage(outputFile)) + .request(new HttpCallbackProxy>(this) { + + @Override + public void onHttpSuccess(@NonNull Xxx data) { + + } + }); +``` + +* This is the upload example, the download is the same, here is no repetition. + +#### Make Synchronous Requests + +* Note: Synchronous requests are time-consuming operations, please ensure that this operation is executed in a sub-thread, do not execute it in the main thread. + +```java +PostRequest postRequest = EasyHttp.post(MainActivity.this); +try { + HttpData data = postRequest + .api(new SearchBlogsApi() + .setKeyword("搬砖不再有")) + .execute(new ResponseClass>() {}); + toast("Request successful, please check the logs"); +} catch (Throwable throwable) { + toast(throwable.getMessage()); +} +``` + +#### Set Request Cache + +* Need to implement interfaces for reading and writing cache, specific encapsulation can refer to [HttpCacheStrategy](app/src/main/java/com/hjq/easy/demo/http/model/HttpCacheStrategy.java) + +* Set cache strategy when initializing the framework + +```java +public final class AppApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + + EasyConfig.with(okHttpClient) + ...... + // Set request cache + .setCacheStrategy(new HttpCacheStrategy()) + ...... + .into(); + } +} +``` + +* If you are using MMKV as the cache implementation for reading and writing, do not forget to initialize MMKV in the application startup + +```java +public final class AppApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + + MMKV.initialize(this); + } +} +``` + +* First, there are four ways to set the cache mode, all of which are in the `CacheMode` enumeration class + +```java +public enum CacheMode { + + /** + * Default (cache according to Http protocol) + */ + DEFAULT, + + /** + * Do not use cache (disable Http protocol cache) + */ + NO_CACHE, + + /** + * Only use cache + * + * If there is already a cache: read cache -> callback success + * If there is no cache: request network -> write cache -> callback success + */ + USE_CACHE_ONLY, + + /** + * Prioritize cache + * + * If there is already a cache: read cache —> callback success —> request network —> refresh cache + * If there is no cache: request network -> write cache -> callback success + */ + USE_CACHE_FIRST, + + /** + * Only read cache when network request fails + */ + USE_CACHE_AFTER_FAILURE +} +``` + +* Set cache mode for a specific interface + +```java +public final class XxxApi implements IRequestApi, IRequestCacheConfig { + + @NonNull + @Override + public String getApi() { + return "xxx/"; + } + + @NonNull + @Override + public CacheMode getCacheMode() { + // Set to prioritize cache + return CacheMode.USE_CACHE_FIRST; + } +} +``` + +* Set global cache mode + +```java +public class XxxServer implements IRequestServer { + + @NonNull + @Override + public String getHost() { + return "https://www.xxxxxxx.com/"; + } + + @NonNull + @Override + public CacheMode getCacheMode() { + // Only read cache when request fails + return CacheMode.USE_CACHE_AFTER_FAILURE; + } +} +``` + +#### Use with Coroutines + +* You can use synchronous requests with coroutines for processing, using the code as follows: + +```kotlin +lifecycleScope.launch(Dispatchers.IO) { + try { + val bean = EasyHttp.post(this@XxxActivity) + .api(XxxApi().apply { + setXxx(xxx) + setXxxx(xxxx) + }) + .execute(object : ResponseClass>() {}) + withContext(Dispatchers.Main) { + // Refresh UI here + } + } catch (throwable: Throwable) { + toast(throwable.message) + } +} +``` + +* If you are not familiar with coroutines, I recommend you read [this article](https://www.jianshu.com/p/2e0746c7d4f3) + +# FAQ + +#### How to Set Cookies + +* EasyHttp is based on OkHttp, and OkHttp itself supports setting cookies, so the usage is the same as OkHttp + +```java +OkHttpClient okHttpClient = new OkHttpClient.Builder() + .cookieJar(new XxxCookieJar()) + .build(); + +EasyConfig.with(okHttpClient) + .setXxx() + .into(); +``` + +#### How to Add or Remove Global Parameters + +* Add global request parameters + +```java +EasyConfig.getInstance().addParam("key", "value"); +``` + +* Remove global request parameters + +```java +EasyConfig.getInstance().removeParam("key"); +``` + +* Add global request headers + +```java +EasyConfig.getInstance().addHeader("key", "value"); +``` + +* Remove global request headers + +```java +EasyConfig.getInstance().removeHeader("key"); +``` + +#### How to Dynamically Add Global Parameters or Headers + +```java +EasyConfig.getInstance().setInterceptor(new IRequestInterceptor() { + + @Override + public void interceptArguments(@NonNull HttpRequest httpRequest, @NonNull HttpParams params, @NonNull HttpHeaders headers) { + // Add request header + headers.put("key", "value"); + // Add parameter + params.put("key", "value"); + } +}); +``` + +#### How to Ignore a Global Parameter in a Request + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "xxx/xxxx"; + } + + @HttpIgnore + private String token; +} +``` + +#### How to Get Server Configuration + +```java +IRequestServer server = EasyConfig.getInstance().getServer(); +// Get the main host address of the current global server +String host = server.getHost(); +``` + +#### How to Modify API Server Configuration + +* First, define a server configuration + +```java +public class XxxServer implements IRequestServer { + + @NonNull + @Override + public String getHost() { + return "https://www.xxxxxxx.com/"; + } +} +``` + +* Then apply it to the global configuration + +```java +EasyConfig.getInstance().setServer(new XxxServer()); +``` + +* If you only want to configure a specific interface, you can do this + +```java +public final class XxxApi extends XxxServer implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "xxx/xxxx"; + } +} +``` + +* If you do not want to define a separate class, you can also write it like this + +```java +public final class XxxApi implements IRequestServer, IRequestApi { + + @NonNull + @Override + public String getHost() { + return "https://www.xxxxxxx.com/"; + } + + @NonNull + @Override + public String getApi() { + return "xxx/xxxx"; + } +} +``` + +#### How to Configure Multiple Domains + +* First, define a test server and official server configuration for a normal interface + +```java +public class TestServer implements IRequestServer { + + @NonNull + @Override + public String getHost() { + return "https://www.test.xxxxxxx.com/"; + } +} +``` + +```java +public class ReleaseServer implements IRequestServer { + + @NonNull + @Override + public String getHost() { + return "https://www.xxxxxxx.com/"; + } +} +``` + +* Then apply it to the global configuration + +```java +IRequestServer server; +if (BuildConfig.DEBUG) { + server = new TestServer(); +} else { + server = new ReleaseServer(); +} +EasyConfig.getInstance().setServer(server); +``` + +* If you want to set a specific server configuration for an H5 business module, you can do this + +```java +public class H5Server implements IRequestServer { + + @NonNull + @Override + public String getHost() { + IRequestServer server = EasyConfig.getInstance().getServer(); + if (server instanceof TestServer) { + return "https://www.test.h5.xxxxxxx.com/"; + } + return "https://www.h5.xxxxxxx.com/"; + } +} +``` + +* You can inherit H5Server when configuring the interface, and other H5 module configurations are similar + +```java +public final class UserAgreementApi extends H5Server implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "user/agreement"; + } +} +``` + +#### How to Change Parameter Submission Method + +* Submit parameters in form format (default) + +```java +public class XxxServer implements IRequestServer { + + @NonNull + @Override + public String getHost() { + return "https://www.xxxxxxx.com/"; + } + + @NonNull + @Override + public IHttpPostBodyStrategy getBodyType() { + return RequestBodyType.FORM; + } +} +``` + +* Submit parameters in Json format + +```java +public class XxxServer implements IRequestServer { + + @NonNull + @Override + public String getHost() { + return "https://www.xxxxxxx.com/"; + } + + @NonNull + @Override + public IHttpPostBodyStrategy getBodyType() { + return RequestBodyType.JSON; + } +} +``` + +* Of course, you can also configure a specific interface separately + +```java +public final class XxxApi implements IRequestApi, IRequestBodyType { + + @NonNull + @Override + public String getApi() { + return "xxx/xxxx"; + } + + @NonNull + @Override + public IHttpPostBodyStrategy getBodyType() { + return RequestBodyType.JSON; + } +} +``` + +* Comparison of advantages and disadvantages of form and Json submission + +| Scenario | Form Method | Json Method | +| :----: | :------: | :-----: | +| Multi-level Parameters | Not Supported | Supported | +| File Upload | Supported | Not Supported | + +#### How to Encrypt or Decrypt API + +* Regarding this issue, it can be implemented using the IRequestInterceptor interface provided by the framework, by overriding the corresponding methods to intercept and modify the content of the object to achieve encryption. + +```java +public interface IRequestInterceptor { + + /** + * Intercept parameters + * + * @param httpRequest Interface object + * @param params Request parameters + * @param headers Request header parameters + */ + default void interceptArguments(@NonNull HttpRequest httpRequest, @NonNull HttpParams params, @NonNull HttpHeaders headers) {} + + /** + * Intercept request header + * + * @param httpRequest Interface object + * @param request Request header object + * @return Return new request header + */ + @NonNull + default Request interceptRequest(@NonNull HttpRequest httpRequest, @NonNull Request request) { + return request; + } + + /** + * Interceptor response header + * + * @param httpRequest Interface object + * @param response Response header object + * @return Return new response header + */ + @NonNull + default Response interceptResponse(@NonNull HttpRequest httpRequest, @NonNull Response response) { + return response; + } +} +``` + +```java +// Set interceptor when initializing the framework +EasyConfig.with(okHttpClient) + // Set request parameter interceptor + .setInterceptor(new XxxInterceptor()) + .into(); +``` + +* If you only want to encrypt or decrypt a specific interface, you can let the Api class implement the IRequestInterceptor interface separately, so it will not follow the global configuration. + +#### How to Ignore a Parameter + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "xxx/xxxx"; + } + + @HttpIgnore + private String address; +} +``` + +#### How to Pass Request Headers + +* Add the `@HttpHeader` annotation to the field, then it means this field is a request header, if no annotation is added, the framework will default to using the field as a request parameter + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "xxx/xxxx"; + } + + @HttpHeader + private String time; +} +``` + +#### How to Pass Dynamic Request Headers + +* Pass a `HashMap` type field, and add the `@HTTPHeader` annotation to it + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "xxx/xxxx"; + } + + @HTTPHeader + private HashMap headers; +} +``` + +#### How to Rename Parameter or Header Name + +* Add the `@HttpRename` annotation to the field, then you can modify the value of the parameter name, if no annotation is added, the framework will default to using the field name as the parameter name + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "xxx/xxxx"; + } + + @HttpRename("k") + private String keyword; +} +``` + +#### How to Upload Files + +* Upload using File object + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "xxx/xxxx"; + } + + private File file; +} +``` + +* Upload using InputStream object + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "xxx/xxxx"; + } + + private InputStream inputStream; +} +``` + +* Upload using RequestBody object + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "xxx/xxxx"; + } + + private RequestBody requestBody; +} +``` + +#### How to Upload a List of Files + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "xxx/xxxx"; + } + + private List files; +} +``` + +#### How to Set Timeout Retry + +```java +// Set request retry count +EasyConfig.getInstance().setRetryCount(3); +// Set request retry time +EasyConfig.getInstance().setRetryTime(1000); +``` + +#### How to Set Request Timeout + +* Global configuration (applies to all interfaces) + +```java +OkHttpClient.Builder builder = new OkHttpClient.Builder(); +builder.readTimeout(5000, TimeUnit.MILLISECONDS); +builder.writeTimeout(5000, TimeUnit.MILLISECONDS); +builder.connectTimeout(5000, TimeUnit.MILLISECONDS); + +EasyConfig.with(builder.build()) + .into(); +``` + +* Local configuration (only applies to a specific interface) + +```java +public final class XxxApi implements IRequestApi, IRequestHttpClient { + + @NonNull + @Override + public String getApi() { + return "xxxx/"; + } + + @NonNull + @Override + public OkHttpClient getOkHttpClient() { + OkHttpClient.Builder builder = EasyConfig.getInstance().getOkHttpClient().newBuilder(); + builder.readTimeout(5000, TimeUnit.MILLISECONDS); + builder.writeTimeout(5000, TimeUnit.MILLISECONDS); + builder.connectTimeout(5000, TimeUnit.MILLISECONDS); + return builder.build(); + } +} +``` + +#### How to Disable Log Printing + +```java +EasyConfig.getInstance().setLogEnabled(false); +``` + +#### How to Change Log Printing Strategy + +* You can first define a class that implements [IHttpLogStrategy](library/src/main/java/com/hjq/http/config/IHttpLogStrategy.java) interface, then pass it in when initializing the framework + +```java +EasyConfig.with(okHttpClient) + ....... + // Set custom log printing strategy + .setLogStrategy(new XxxStrategy()) + .into(); +``` + +* Scenarios requiring modification of log printing strategy + + * Need to write request logs to local + + * Need to modify the format of printed request logs + +#### How to Cancel an Ongoing Request + +```java +// Cancel request task based on TAG +EasyHttp.cancelByTag(Object tag); +// Cancel request with specified Tag +EasyHttp.cancelByTag(Object tag); +// Cancel all requests +EasyHttp.cancelAll(); +``` + +#### How to Delay a Request + +```java +EasyHttp.post(MainActivity.this) + .api(new XxxApi()) + // Request after 5 seconds + .delay(5000) + .request(new HttpCallbackProxy>(this) { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + }); +``` + +#### How to Dynamically Concatenate API Path + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "article/query/" + pageNumber + "/json"; + } + + @HttpIgnore + private int pageNumber; + + public XxxApi setPageNumber(int pageNumber) { + this.pageNumber = pageNumber; + return this; + } +} +``` + +#### How to Dynamically Set the Entire Request URL + +```java +EasyHttp.post(this) + .api(new RequestUrl("https://xxxx.com/aaaa")) + .request(new HttpCallbackProxy(this) { + + @Override + public void onHttpSuccess(@NonNull Xxx result) { + + } + }); +``` + +#### How to Trust All Certificates for HTTPS + +* Set this when initializing OkHttp + +```java +HttpSslConfig sslConfig = HttpSslFactory.generateSslConfig(); +OkHttpClient okHttpClient = new OkHttpClient.Builder() + .sslSocketFactory(sslConfig.getsSLSocketFactory(), sslConfig.getTrustManager()) + .hostnameVerifier(HttpSslFactory.generateUnSafeHostnameVerifier()) + .build(); +``` +* However, this is not recommended, as this is insecure, meaning that no Https verification will be used for each request. + +* Of course, the framework also provides some API for generating certificates, please refer to the classes under the com.hjq.http.ssl package. + +#### What if I Don't Want to Write a Class for Each API + +* First, define a URL management class, and configure the URL in this class + +```java +public final class HttpUrls { + + /** Get user info */ + public static final String GET_USER_INFO = "user/getUserInfo"; +} +``` + +* Then introduce the interface path into EasyHttp + +```java +EasyHttp.post(this) + .api(HttpUrls.GET_USER_INFO) + .request(new HttpCallbackProxy>(this) { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + }); +``` + +* However, this method can only be applied to interfaces without parameters, interfaces with parameters still need to write a class, because the framework only parses parameters in the Api class. + +* Although EasyHttp opens this writing method, as the author, I do not recommend you to write it this way, because this writing method will result in poor extensibility, for example, subsequent addition of parameters, you still need to change it back, and cannot dynamically configure the interface. + +#### What if the Framework Only Accepts LifecycleOwner + +* Among them, `androidx.appcompat.app.AppCompatActivity` and `androidx.fragment.app.Fragment` are subclasses of LifecycleOwner, this is unquestionable, you can directly pass it to the framework as LifecycleOwner + +* However, if you pass in an `android.app.Activity` object, which is not an `androidx.appcompat.app.AppCompatActivity` object, then you can write it like this + +```java +EasyHttp.post(new ActivityLifecycle(this)) + .api(new XxxApi()) + .request(new HttpCallbackProxy>(this) { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + }); +``` + +* If you pass in an `android.app.Fragment` object, which is not an `androidx.fragment.app.Fragment` object, please directly inherit the LifecycleAppFragment class in the framework, or encapsulate a Fragment base class with Lifecycle in your project + +* If you want to use EasyHttp in an `android.app.Service`, please directly inherit the LifecycleService class in the framework, or encapsulate a Service base class with Lifecycle in your project + +* If none of the above conditions are met, but you still want to request the network in a certain place, then you can write it like this + +```java +EasyHttp.post(ApplicationLifecycle.getInstance()) + .api(new XxxApi()) + .tag("abc") + .request(new OnHttpListener>() { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + + @Override + public void onHttpFail(@NonNull Throwable throwable) { + + } + }); +``` + +* Note: Passing ApplicationLifecycle means that the framework cannot automatically control the lifecycle of the request, if you write it in Application, it is completely fine, but you cannot write it in Activity or Service, because this may lead to memory leaks. + +* In addition to Application, if you use the writing method of ApplicationLifecycle in Activity or Service, in order to avoid memory leaks or crashes, you need to set the corresponding Tag when requesting, and manually cancel the request at the appropriate time (usually when Activity or Service is destroyed or exited). + +```java +EasyHttp.cancelByTag("abc"); +``` + +#### How to Use EasyHttp in ViewModel + +* Step 1: Encapsulate a BaseViewModel, and incorporate the LifecycleOwner feature + +```java +public class BaseViewModel extends ViewModel implements LifecycleOwner { + + private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); + + public BaseViewModel() { + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + } + + @Override + protected void onCleared() { + super.onCleared(); + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + return mLifecycle; + } +} +``` + +* Step 2: Let the business ViewModel class inherit BaseViewModel, specific example as follows + +```java +public class XxxViewModel extends BaseViewModel { + + public void xxxx() { + EasyHttp.post(this) + .api(new XxxApi()) + .request(new OnHttpListener>() { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + + @Override + public void onHttpFail(@NonNull Throwable throwable) { + + } + }); + } +} +``` + +#### How to Cancel the Loading Dialog When Cancelling a Request + +* First, this loading dialog is not built-in by the framework, it can be modified or cancelled, there are two ways to choose from + +* First way: Override HttpCallbackProxy callback methods + +```java +EasyHttp.post(this) + .api(new XxxApi()) + .request(new HttpCallbackProxy(this) { + + @Override + public void onHttpStart(@NonNull IRequestApi api) { + // Override method and comment out parent call + //super.onHttpStart(call); + } + + @Override + public void onHttpEnd(@NonNull IRequestApi api) { + // Override method and comment out parent call + //super.onHttpEnd(call); + } + }); +``` + +* Second way: Directly implement OnHttpListener interface + +```java + +EasyHttp.post(this) + .api(new XxxApi()) + .request(new OnHttpListener() { + + @Override + public void onHttpSuccess(@NonNull Xxx result) { + + } + + @Override + public void onHttpFail(@NonNull Throwable throwable) { + + } + }); +``` + +#### How to Upload a JSON Array as a Parameter + +* Since the Api class will eventually be converted into a JsonObject string, if you need to upload a JsonArray string, please implement it in the following way + +```java +List parameter = new ArrayList(); +list.add(xxx); +list.add(xxx); +String json = gson.toJson(parameter); + +EasyHttp.post(this) + .api(new XxxApi()) + .body(new JsonRequestBody(json)) + .request(new HttpCallbackProxy>(this) { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + }); +``` + +* However, I personally do not recommend using JsonArray as the root type of parameters, because the extensibility of such interfaces is extremely poor. + +#### What if the Key of API Parameter is Dynamic + +* The framework parses the fields in the Api class to be parameters through reflection, the field name as the parameter Key, the field value as the parameter Value, since Java cannot dynamically change the field name, so it cannot be modified through normal means, if you have this requirement, please implement it through the following means + +```java +HashMap parameter = new HashMap(); + +// Add global parameters +HashMap globalParams = EasyConfig.getInstance().getParams(); +Set keySet = globalParams.keySet(); +for (String key : keySet) { + parameter.put(key, globalParams.get(key)); +} + +// Add custom parameters +parameter.put("key1", value1); +parameter.put("key2", value2); + +String json = gson.toJson(parameter); +JsonRequestBody jsonRequestBody = new JsonRequestBody(json) + +EasyHttp.post(this) + .api(new XxxApi()) + .body(jsonRequestBody) + .request(new HttpCallbackProxy>(this) { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + }); +``` + +#### How to Set a Custom UA Identifier + +* First, UA stands for User Agent, when we do not set a custom UA identifier, then OkHttp will add a default UA identifier in the BridgeInterceptor, then how to set a custom UA identifier in EasyHttp? It's actually very simple, UA identifier is essentially a request header, add a request header with the name `User-Agent` to EasyHttp, and then how to add a request header, the previous documentation has already been introduced, here is no repetition. + +#### How to Change the Thread for Request Callback + +```java +EasyHttp.post(this) + .api(new XxxApi()) + // Indicates that the callback is performed in a sub-thread + .schedulers(ThreadSchedulers.IO) + .request(new HttpCallbackProxy>(this) { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + }); +``` + +#### How to Use a Custom RequestBody for Requests + +* In some extreme cases, the framework cannot meet the needs, at which point a custom `RequestBody` is needed, then how to use a custom `RequestBody`? The framework actually has an open method, the specific usage example is as follows: + +```java +EasyHttp.post(this) + .api(new XxxApi()) + .body(RequestBody body) + .request(new HttpCallbackProxy>(this) { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + }); +``` + +* Note: Since Post requests place parameters on the `RequestBody`, and a request can only set one `RequestBody`, if you set a custom `body(RequestBody body)`, then the framework will not parse the fields in the `XxxApi` class into parameters. In addition to Post requests, Put requests and Patch requests can also use this method to set, here is no repetition. + +#### How to Customize ContentType in Request Header + +* The specific writing example is as follows: + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "xxx/xxx"; + } + + @HttpHeader + @HttpRename("Content-Type") + private String contentType = "application/x-www-form-urlencoded;charset=utf-8"; +} +``` + +* Note: This function was added in framework **11.5** version, previous versions did not have this function. + +#### How to Customize Key and Value in Get Request Parameters + +* First, define a custom Api class, then use the `getApi` method to dynamically concatenate the parameters + +```java +public final class CustomParameterApi implements IRequestApi { + + @HttpIgnore + @NonNull + private final Map parameters; + + public CustomParameterApi() { + this(new HashMap()); + } + + public CustomParameterApi(@NonNull Map parameters) { + this.parameters = parameters; + } + + @NonNull + @Override + public String getApi() { + Set keys = parameters.keySet(); + + StringBuilder builder = new StringBuilder(); + int index = 0; + for (String key : keys) { + String value = parameters.get(key); + + if (index == 0) { + builder.append("?"); + } + builder.append(key) + .append("=") + .append(value); + if (index < keys.size() - 1) { + builder.append("&"); + } + index++; + } + + return "xxx/xxx" + builder; + } + + public CustomParameterApi putParameter(String key, String value) { + parameters.put(key, value); + return this; + } + + public CustomParameterApi removeParameter(String key) { + parameters.remove(key); + return this; + } +} +``` + +* The outer layer can call it in the following way + +```java +CustomParameterApi api = new CustomParameterApi(); +api.putParameter("key1", "value1"); +api.putParameter("key2", "value2"); + +EasyHttp.get(this) + .api(api) + .request(new HttpCallbackProxy(this) { + + @Override + public void onHttpSuccess(@NonNull Xxx result) { + + } + }); +``` + +* Note: This implementation method is only applicable in cases where the framework's design cannot meet the requirements, and the author does not recommend using this method, because this method is not convenient to manage the key of request parameters, it is recommended to use the method of defining fields on the class to achieve. + +#### How to Define Get-like Parameters in Post Request + +* Directly concatenate request parameters to the url, and ignore the value of a field (to avoid being parsed as Post parameters) + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "article/query?pageNumber=" + pageNumber; + } + + @HttpIgnore + private int pageNumber; + + public XxxApi setPageNumber(int pageNumber) { + this.pageNumber = pageNumber; + return this; + } +} +``` + +* Ps: In general, I do not recommend writing it this way, this request looks awkward, it looks like a Get request, but in fact it is a Post request. + +# Use with RxJava + +#### Preparation + +* Add remote dependency + +```groovy +dependencies { + // RxJava: https://github.com/ReactiveX/RxJava + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation 'io.reactivex.rxjava2:rxjava:2.2.12' +} +``` + +* Note: RxJava needs to handle lifecycle yourself to avoid memory leaks + +#### Multiple Requests in Series + +```java +Observable.create(new ObservableOnSubscribe>() { + + @Override + public void subscribe(ObservableEmitter> emitter) throws Exception { + + HttpData data1; + try { + data1 = EasyHttp.post(MainActivity.this) + .api(new SearchBlogsApi() + .setKeyword("搬砖不再有")) + .execute(new ResponseClass>() {}); + } catch (Throwable throwable) { + if (throwable instanceof Exception) { + throw (Exception) throwable; + } else { + throw new RuntimeException(throwable); + } + } + + HttpData data2; + try { + data2 = EasyHttp.post(MainActivity.this) + .api(new SearchBlogsApi() + .setKeyword(data1.getMessage())) + .execute(new ResponseClass>() {}); + } catch (Throwable throwable) { + if (throwable instanceof Exception) { + throw (Exception) throwable; + } else { + throw new RuntimeException(throwable); + } + } + + emitter.onNext(data2); + emitter.onComplete(); + } +}) +// Let the observer execute in IO thread +.subscribeOn(Schedulers.io()) +// Let the observer execute in the main thread +.observeOn(AndroidSchedulers.mainThread()) +.subscribe(new Consumer>() { + + @Override + public void accept(HttpData data) throws Exception { + Log.i("EasyHttp", "Final result: " + data.getMessage()); + } + +}, new Consumer() { + + @Override + public void accept(Throwable throwable) throws Exception { + toast(throwable.getMessage()); + } +}); +``` + +#### Polling Requests + +* If the polling times are limited, you can consider using Http requests to implement it, but if the polling times are infinite, then it is not recommended to use Http requests to implement it, it should be done using WebSocket, or other long-link protocols. + +```java +// Initiate polling request, initiate three requests, the first request triggers after 5 seconds, the remaining two trigger after 1 second and 2 seconds +Observable.intervalRange(1, 3, 5000, 1000, TimeUnit.MILLISECONDS) + // Let the observer execute in IO thread + .subscribeOn(Schedulers.io()) + // Let the observer execute in the main thread + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(Long aLong) throws Exception { + EasyHttp.post(MainActivity.this) + .api(new SearchBlogsApi() + .setKeyword("搬砖不再有")) + .request(new HttpCallbackProxy>(MainActivity.this) { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + }); + } + }); +``` + +#### Wrap Returned Data + +```java +Observable.create(new ObservableOnSubscribe>() { + + @Override + public void subscribe(ObservableEmitter> emitter) throws Exception { + EasyHttp.post(MainActivity.this) + .api(new SearchBlogsApi() + .setKeyword("搬砖不再有")) + .request(new HttpCallbackProxy>(MainActivity.this) { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + emitter.onNext(result); + emitter.onComplete(); + } + + @Override + public void onHttpFail(@NonNull Throwable throwable) { + super.onHttpFail(throwable); + emitter.onError(throwable); + } + }); + } +}) +.map(new Function, String>() { + + @Override + public String apply(HttpData data) throws Exception { + int curPage = data.getData().getCurPage(); + int pageCount = data.getData().getPageCount(); + return curPage + "/" + pageCount; + } +}) +// Let the observer execute in IO thread +.subscribeOn(Schedulers.io()) +// Let the observer execute in the main thread +.observeOn(AndroidSchedulers.mainThread()) +.subscribe(new Consumer() { + + @Override + public void accept(String s) throws Exception { + Log.i("EasyHttp", "Current page position: " + s); + } + +}, new Consumer() { + + @Override + public void accept(Throwable throwable) throws Exception { + toast(throwable.getMessage()); + } +}); +``` + +# Support Protobuf + +#### Preparation + +* Add the following configuration to the `build.gradle` file in the project root directory + +```groovy +buildscript { + + ...... + + dependencies { + // Auto-generate Protobuf class plugin: https://github.com/google/protobuf-gradle-plugin + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.9.3' + } +} +``` + +* Add remote dependency in the `build.gradle` file of the app module + +```groovy +...... + +apply plugin: 'com.google.protobuf' + +android { + ...... + + sourceSets { + main { + proto { + // Specify Protobuf file path + srcDir 'src/main/proto' + } + } + } +} + +protobuf { + + protoc { + // You can also configure the local compiler path + artifact = 'com.google.protobuf:protoc:3.23.0' + } + + generateProtoTasks { + all().each { task -> + task.builtins { + remove java + } + task.builtins { + // Produce java source code + java {} + } + } + } +} + +dependencies { + + ...... + + // Protobuf: https://github.com/protocolbuffers/protobuf + implementation 'com.google.protobuf:protobuf-java:3.23.1' + implementation 'com.google.protobuf:protoc:3.23.0' +} +``` + +* Create a folder named `proto` under `app/src/main/`, used to store Protobuf related files, then create a file `Person.proto`, the specific content is as follows: + +```text +syntax = "proto3"; +package tutorial; + +message Person { + string name = 1; + int32 id = 2; + string email = 3; + string phone = 4; +} +``` + +* Then `Rebuild Project`, you can see the plugin automatically generate the `PersonOuterClass` class, `PersonOuterClass` class also has a static inner class named `Person` + +#### Parse Request Body as Protobuf + +* Create a custom RequestBody class to parse Protocol objects into streams, it is recommended to be placed in the `com.xxx.xxx/http/model` package, + +```java +public class ProtocolRequestBody extends RequestBody { + + /** MessageLite object */ + private final MessageLite mMessageLite; + /** Byte array */ + private final byte[] mBytes; + + public ProtocolRequestBody(MessageLite messageLite) { + mMessageLite = messageLite; + mBytes = messageLite.toByteArray(); + } + + @Override + public MediaType contentType() { + return ContentType.JSON; + } + + @Override + public long contentLength() { + // Note: This needs to be calculated using the length of the byte array + return mBytes.length; + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + sink.write(mBytes, 0, mBytes.length); + } + + @NonNull + @Override + public String toString() { + return mMessageLite.toString(); + } + + /** + * Get MessageLite object + */ + @NonNull + public MessageLite getMessageLite() { + return mMessageLite; + } +} +``` + +* Example of initiating a request + +```java +// Pretend to generate a Protobuf object +Person person = Person.parseFrom("xxxxxxxxx".getBytes()); + +EasyHttp.post(this) + .api(new XxxApi()) + .body(new ProtocolRequestBody(person)) + .request(new HttpCallbackProxy>(this) { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + }); +``` + +#### Parse Response Body as Protobuf + +* This support is very simple, just modify the implementation of the `IRequestHandler` interface, the specific code implementation is as follows: + +``` +public final class RequestHandler implements IRequestHandler { + + ...... + + @NonNull + @Override + public Object requestSuccess(@NonNull HttpRequest httpRequest, @NonNull Response response, + @NonNull Type type) throws Throwable { + ...... + + final Object result; + + try { + if (type instanceof Class && AbstractParser.class.isAssignableFrom((Class) type)) { + String simpleName = ((Class) type).getSimpleName(); + Class clazz = Class.forName("tutorial." + simpleName + ".OuterClass." + simpleName); + Method parseFromMethod = clazz.getMethod("parseFrom", byte[].class); + // Call static method + result = parseFromMethod.invoke(null, (Object) text.getBytes()); + } else { + result = GsonFactory.getSingletonGson().fromJson(text, type); + } + } catch (JsonSyntaxException e) { + // Return result read exception + throw new DataException(mApplication.getString(R.string.http_data_explain_error), e); + } + + ...... + return result; + } +} +``` \ No newline at end of file diff --git a/HelpDoc.md b/HelpDoc.md index 84acea2..7524d6c 100644 --- a/HelpDoc.md +++ b/HelpDoc.md @@ -10,8 +10,6 @@ * [框架初始化](#框架初始化) - * [混淆规则](#混淆规则) - * [使用文档](#使用文档) * [配置接口](#配置接口) @@ -22,15 +20,21 @@ * [下载文件](#下载文件) - * [同步请求](#同步请求) + * [分区存储适配](#分区存储适配) + + * [发起同步请求](#发起同步请求) - * [请求缓存](#请求缓存) + * [设置请求缓存](#设置请求缓存) + + * [搭配协程使用](#搭配协程使用) * [疑难解答](#疑难解答) - * [如何添加全局参数](#如何添加全局参数) + * [如何设置 Cookie](#如何设置-cookie) + + * [如何添加或者删除全局参数](#如何添加或者删除全局参数) - * [如何定义全局的动态参数](#如何定义全局的动态参数) + * [如何动态添加全局的参数或者请求头](#如何动态添加全局的参数或者请求头) * [如何在请求中忽略某个全局参数](#如何在请求中忽略某个全局参数) @@ -42,12 +46,14 @@ * [如何修改参数的提交方式](#如何修改参数的提交方式) - * [如何对整个 Json 字符串进行加密](#如何对整个-json-字符串进行加密) + * [如何对接口进行加密或者解密](#如何对接口进行加密或者解密) * [如何忽略某个参数](#如何忽略某个参数) * [如何传入请求头](#如何传入请求头) + * [如何传入动态的请求头](#如何传入动态的请求头) + * [如何重命名参数或者请求头的名称](#如何重命名参数或者请求头的名称) * [如何上传文件](#如何上传文件) @@ -60,34 +66,60 @@ * [如何设置不打印日志](#如何设置不打印日志) - * [如何取消已发起的请求](#如何取消已发起的请求) + * [如何修改日志打印策略](#如何修改日志打印策略) - * [如何延迟发起一个请求](如何延迟发起一个请求) + * [如何取消已发起的请求](#如何取消已发起的请求) - * [如何对请求头和响应头进行加解密](#如何对请求头和响应头进行加解密) + * [如何延迟发起一个请求](#如何延迟发起一个请求) * [如何对接口路径进行动态化拼接](#如何对接口路径进行动态化拼接) + * [如何动态化整个请求的 url](#如何动态化整个请求的-url) + * [Https 如何配置信任所有证书](#https-如何配置信任所有证书) * [我不想一个接口写一个类怎么办](#我不想一个接口写一个类怎么办) * [框架只能传入 LifecycleOwner 该怎么办](#框架只能传入-lifecycleowner-该怎么办) + * [如何在 ViewModel 中使用 EasyHttp 请求网络](#如何在-viewmodel-中使用-easyhttp-请求网络) + * [我想取消请求时显示的加载对话框该怎么办](#我想取消请求时显示的加载对话框该怎么办) + * [我想用 Json 数组作为参数进行上传该怎么办](#我想用-json-数组作为参数进行上传该怎么办) + + * [接口参数的 Key 值是动态变化的该怎么办](#接口参数的-key-值是动态变化的该怎么办) + + * [如何设置自定义的 UA 标识](#如何设置自定义的-ua-标识) + + * [我想修改请求回调所在的线程该怎么办](#我想修改请求回调所在的线程该怎么办) + + * [我想自定义一个 RequestBody 进行请求该怎么办](#我想自定义一个-requestbody-进行请求该怎么办) + + * [我想自定义请求头中的 ContentType 该怎么做](#我想自定义请求头中的-contenttype-该怎么做) + + * [我想自定义 Get 请求参数中的 key 和 value 该怎么做](#我想自定义-get-请求参数中的-key-和-value-该怎么做) + + * [我想在 Post 请求中定义类似 Get 请求参数该怎么做](#我想在-post-请求中定义类似-get-请求参数该怎么做) + * [搭配 RxJava](#搭配-rxjava) * [准备工作](#准备工作) * [多个请求串行](#多个请求串行) - * [多个请求并行](#多个请求并行) - * [发起轮询请求](#发起轮询请求) * [对返回的数据进行包装](#对返回的数据进行包装) +* [支持 Protobuf](#支持-protobuf) + + * [准备工作](#准备工作) + + * [请求体解析成 Protobuf](#请求体解析成-protobuf) + + * [响应体解析支持 Protobuf](#响应体解析支持-protobuf) + # 集成文档 #### 配置权限 @@ -104,7 +136,8 @@ #### Http 明文请求 * **Android 9.0** 限制了明文流量的网络请求,非加密的流量请求都会被系统禁止掉。 -如果当前应用的请求是 http 请求,而非 https ,这样就会导系统禁止当前应用进行该请求,如果 WebView 的 url 用 http 协议,同样会出现加载失败,https 不受影响 + +* 如果当前应用的请求是 http 请求,而非 https,这样就会导系统禁止当前应用进行该请求,如果 WebView 的 url 用 http 协议,同样会出现加载失败,https 不受影响 * 在 res 下新建一个 xml 目录,然后创建一个名为:`network_security_config.xml` 文件 ,该文件内容如下 @@ -115,7 +148,7 @@ ``` -* 然后在 AndroidManifest.xml application 标签内应用上面的xml配置 +* 然后在 AndroidManifest.xml application 标签内应用上面的 xml 配置 ```xml ; -} -``` - # 使用文档 #### 配置接口 @@ -201,6 +216,7 @@ EasyConfig.getInstance() ```java public final class LoginApi implements IRequestApi { + @NonNull @Override public String getApi() { return "user/login"; @@ -230,19 +246,17 @@ public final class LoginApi implements IRequestApi { * @HttpIgnore:标记这个字段不会被发送给后台 - * @HttpRename:重新定义这个字段发送给后台的参数名称 + * @HttpRename:重新定义这个字段发送给后台的参数或者请求头名称 * 可在这个类实现一些接口 * implements IRequestHost:实现这个接口之后可以重新指定这个请求的主机地址 - * implements IRequestPath:实现这个接口之后可以重新指定这个请求的接口路径 - - * implements IRequestType:实现这个接口之后可以重新指定这个请求的提交方式 + * implements IRequestBodyType:实现这个接口之后可以重新指定这个请求体的提交方式 - * implements IRequestCache:实现这个接口之后可以重新指定这个请求的缓存模式 + * implements IRequestCacheConfig:实现这个接口之后可以重新指定这个请求的缓存模式配置 - * implements IRequestClient:实现这个接口之后可以重新指定这个请求所用的 OkHttpClient 对象 + * implements IRequestHttpClient:实现这个接口之后可以重新指定这个请求所用的 OkHttpClient 对象 * 字段作为请求参数的衡量标准 @@ -252,29 +266,18 @@ public final class LoginApi implements IRequestApi { * 假设某个字段类型是 int,因为基本数据类型没有空值,所以这个字段一定会作为请求参数,但是可以换成 Integer 对象来避免,因为 Integer 的默认值是 null -* getHost、getPath、getApi 方法之间的作用和区别? - - * Host:主机地址 - - * Path:模块地址 - - * Api:业务地址 - * 我举个栗子:[https://www.baidu.com/api/user/getInfo](https://www.baidu.com/),那么标准的写法就是 ```java public final class XxxApi implements IRequestServer, IRequestApi { + @NonNull @Override public String getHost() { return "https://www.baidu.com/"; } - @Override - public String getPath() { - return "api/"; - } - + @NonNull @Override public String getApi() { return "user/getInfo"; @@ -284,18 +287,18 @@ public final class XxxApi implements IRequestServer, IRequestApi { #### 发起请求 -* 需要配置请求状态及生命周期处理,具体封装可以参考 [BaseActivity](app/src/main/java/com/hjq/http/demo/BaseActivity.java) +* 需要配置请求状态及生命周期处理,具体封装可以参考 [BaseActivity](app/src/main/java/com/hjq/easy/demo/BaseActivity.java) ```java EasyHttp.post(this) .api(new LoginApi() .setUserName("Android 轮子哥") .setPassword("123456")) - .request(new HttpCallback>(activity) { + .request(new HttpCallbackProxy>(activity) { @Override - public void onSucceed(HttpData data) { - ToastUtils.show("登录成功"); + public void onHttpSuccess(@NonNull HttpData data) { + toast("登录成功"); } }); ``` @@ -305,17 +308,19 @@ EasyHttp.post(this) #### 上传文件 ```java -public final class UpdateImageApi implements IRequestApi, IRequestType { +public final class UpdateImageApi implements IRequestApi, IRequestBodyType { + @NonNull @Override public String getApi() { return "upload/"; } + @NonNull @Override - public BodyType getType() { + public IHttpPostBodyStrategy getBodyType() { // 上传文件需要使用表单的形式提交 - return BodyType.FORM; + return RequestBodyType.FORM; } /** 本地图片 */ @@ -338,27 +343,27 @@ EasyHttp.post(this) .request(new OnUpdateListener() { @Override - public void onStart(Call call) { + public void onUpdateStart(@NonNull IRequestApi api) { mProgressBar.setVisibility(View.VISIBLE); } @Override - public void onProgress(int progress) { + public void onUpdateProgressChange(int progress) { mProgressBar.setProgress(progress); } @Override - public void onSucceed(Void result) { - ToastUtils.show("上传成功"); + public void onUpdateSuccess(@NonNull Void result) { + toast("上传成功"); } @Override - public void onFail(Exception e) { - ToastUtils.show("上传失败"); + public void onUpdateFail(@NonNull Throwable throwable) { + toast("上传失败"); } @Override - public void onEnd(Call call) { + public void onUpdateEnd(@NonNull IRequestApi api) { mProgressBar.setVisibility(View.GONE); } }); @@ -366,6 +371,8 @@ EasyHttp.post(this) * **需要注意的是:如果上传的文件过多或者过大,可能会导致请求超时,可以重新设置本次请求超时时间,超时时间建议根据文件大小而定,具体设置超时方式文档有介绍,可以在本页面直接搜索。** +* 当然除了可以使用 `File` 类型的对象进行上传,还可以使用 `FileContentResolver`、`InputStream`、`RequestBody`、`MultipartBody.Part` 类型的对象进行上传,如果你需要批量上传,请使用 `List`、`List`、`List`、`List`、`List`、 类型的对象来做批量上传。 + #### 下载文件 * 下载缓存策略:在指定下载文件 md5 或者后台有返回 md5 的情况下,下载框架默认开启下载缓存模式,如果这个文件已经存在手机中,并且经过 md5 校验文件完整,框架就不会重复下载,而是直接回调下载监听。减轻服务器压力,减少用户等待时间。 @@ -377,95 +384,121 @@ EasyHttp.download(this) //.url("https://qd.myapp.com/myapp/qqteam/AndroidQQ/mobileqq_android.apk") .url("http://dldir1.qq.com/weixin/android/weixin708android1540.apk") .md5("2E8BDD7686474A7BC4A51ADC3667CABF") + // 设置断点续传(默认不开启) + //.resumableTransfer(true) .listener(new OnDownloadListener() { @Override - public void onStart(File file) { + public void onDownloadStart(@NonNull File file) { mProgressBar.setVisibility(View.VISIBLE); } @Override - public void onProgress(File file, int progress) { + public void onDownloadProgressChange(@NonNull File file, int progress) { mProgressBar.setProgress(progress); } @Override - public void onComplete(File file) { - ToastUtils.show("下载完成:" + file.getPath()); - installApk(MainActivity.this, file); + public void onDownloadSuccess(@NonNull File file) { + toast("下载完成:" + file.getPath()); + installApk(XxxActivity.this, file); } @Override - public void onError(File file, Exception e) { - ToastUtils.show("下载出错:" + e.getMessage()); + public void onDownloadFail(@NonNull File file, @NonNull Throwable throwable) { + toast("下载失败:" + throwable.getMessage()); + file.delete(); } @Override - public void onEnd(File file) { + public void onDownloadEnd(@NonNull File file) { mProgressBar.setVisibility(View.GONE); } }).start(); ``` -#### 同步请求 +#### 分区存储适配 + +* 在 Android 10 之前,我们在读写外部存储的时候,可以直接使用 File 对象来上传或者下载文件,但是在 Android 10 之后,如果你的项目需要 Android 10 分区存储的特性,那么在读写外部存储文件的时候,就不能直接使用 File 对象,因为 `ContentResolver.insert` 返回是一个 `Uri` 对象,这个时候就需要使用到框架提供的 `FileContentResolver` 对象了(这个对象是 File 的子类),具体使用案例如下: + +```java +File outputFile; +if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.Q) { + ContentValues values = new ContentValues(); + ......... + // 生成一个新的 uri 路径 + Uri outputUri = getContentResolver().insert(MediaStore.Xxx.Media.EXTERNAL_CONTENT_URI, values); + // 适配 Android 10 分区存储特性 + outputFile = new FileContentResolver(context, outputUri); +} else { + outputFile = new File(xxxx); +} + +EasyHttp.post(this) + .api(new XxxApi() + .setImage(outputFile)) + .request(new HttpCallbackProxy>(this) { + + @Override + public void onHttpSuccess(@NonNull Xxx data) { + + } + }); +``` + +* 这是上传的案例,下载也同理,这里不再赘述。 + +#### 发起同步请求 + +* 需要注意的是:同步请求是耗时操作,不能在主线程中执行,请务必保证此操作在子线程中执行 ```java +PostRequest postRequest = EasyHttp.post(MainActivity.this); try { - HttpData data = EasyHttp.post(MainActivity.this) + HttpData data = postRequest .api(new SearchBlogsApi() .setKeyword("搬砖不再有")) .execute(new ResponseClass>() {}); - ToastUtils.show("请求成功,请看日志"); -} catch (Exception e) { - e.printStackTrace(); - ToastUtils.show(e.getMessage()); + toast("请求成功,请看日志"); +} catch (Throwable throwable) { + toast(throwable.getMessage()); } ``` -#### 请求缓存 +#### 设置请求缓存 -* 需要先实现读取和写入缓存的接口,如果已配置则可以跳过,这里以 MMKV 为例 +* 需要先实现读取和写入缓存的接口,具体封装可以参考 [HttpCacheStrategy](app/src/main/java/com/hjq/easy/demo/http/model/HttpCacheStrategy.java) -```java -public final class RequestHandler implements IRequestHandler { +* 在初始化框架的代码时设置缓存策略 - private final Application mApplication; - private final MMKV mMmkv; - - public RequestHandler(Application application) { - mApplication = application; - mMmkv = MMKV.mmkvWithID("http_cache_id"); - } - - .................. +```java +public final class AppApplication extends Application { @Override - public Object readCache(LifecycleOwner lifecycle, IRequestApi api, Type type) { - String cacheKey = GsonFactory.getSingletonGson().toJson(api); - String cacheValue = mMmkv.getString(cacheKey, null); - if (cacheValue == null || "".equals(cacheValue) || "{}".equals(cacheValue)) { - return null; - } - EasyLog.print("---------- cacheKey ----------"); - EasyLog.json(cacheKey); - EasyLog.print("---------- cacheValue ----------"); - EasyLog.json(cacheValue); - return GsonFactory.getSingletonGson().fromJson(cacheValue, type); + public void onCreate() { + super.onCreate(); + + EasyConfig.with(okHttpClient) + ...... + // 设置请求缓存 + .setCacheStrategy(new HttpCacheStrategy()) + ...... + .into(); } +} +``` + +* 如果你使用的是 MMKV 作为读写缓存实现,也不要忘记在应用启动的时候初始化 MMKV + +```java +public final class AppApplication extends Application { @Override - public boolean writeCache(LifecycleOwner lifecycle, IRequestApi api, Response response, Object result) { - String cacheKey = GsonFactory.getSingletonGson().toJson(api); - String cacheValue = GsonFactory.getSingletonGson().toJson(result); - if (cacheValue == null || "".equals(cacheValue) || "{}".equals(cacheValue)) { - return false; - } - EasyLog.print("---------- cacheKey ----------"); - EasyLog.json(cacheKey); - EasyLog.print("---------- cacheValue ----------"); - EasyLog.json(cacheValue); - return mMmkv.putString(cacheKey, cacheValue).commit(); + public void onCreate() { + super.onCreate(); + + MMKV.initialize(this); } } ``` @@ -488,16 +521,16 @@ public enum CacheMode { /** * 只使用缓存 * - * 有缓存的情况下:读取缓存 -> 回调成功 - * 无缓存的情况下:请求网络 -> 写入缓存 -> 回调成功 + * 已有缓存的情况下:读取缓存 -> 回调成功 + * 没有缓存的情况下:请求网络 -> 写入缓存 -> 回调成功 */ USE_CACHE_ONLY, /** * 优先使用缓存 * - * 有缓存的情况下:先读缓存 —> 回调成功 —> 请求网络 —> 刷新缓存 - * 无缓存的情况下:请求网络 -> 写入缓存 -> 回调成功 + * 已有缓存的情况下:先读缓存 —> 回调成功 —> 请求网络 —> 刷新缓存 + * 没有缓存的情况下:请求网络 -> 写入缓存 -> 回调成功 */ USE_CACHE_FIRST, @@ -511,15 +544,17 @@ public enum CacheMode { * 为某个接口设置缓存模式 ```java -public final class XxxApi implements IRequestApi, IRequestCache { +public final class XxxApi implements IRequestApi, IRequestCacheConfig { + @NonNull @Override public String getApi() { return "xxx/"; } + @NonNull @Override - public CacheMode getMode() { + public CacheMode getCacheMode() { // 设置优先使用缓存 return CacheMode.USE_CACHE_FIRST; } @@ -531,38 +566,98 @@ public final class XxxApi implements IRequestApi, IRequestCache { ```java public class XxxServer implements IRequestServer { + @NonNull @Override public String getHost() { return "https://www.xxxxxxx.com/"; } + @NonNull @Override - public CacheMode getMode() { + public CacheMode getCacheMode() { // 只在请求失败才去读缓存 return CacheMode.USE_CACHE_AFTER_FAILURE; } } ``` +#### 搭配协程使用 + +* 可以使用同步请求搭配协程做处理,使用代码如下: + +```kotlin +lifecycleScope.launch(Dispatchers.IO) { + try { + val bean = EasyHttp.post(this@XxxActivity) + .api(XxxApi().apply { + setXxx(xxx) + setXxxx(xxxx) + }) + .execute(object : ResponseClass>() {}) + withContext(Dispatchers.Main) { + // 在这里进行 UI 刷新 + } + } catch (throwable: Throwable) { + toast(throwable.message) + } +} +``` + +* 如果你对协程的使用不太熟悉,推荐你看一下[这篇文章](https://www.jianshu.com/p/2e0746c7d4f3) + # 疑难解答 -#### 如何添加全局参数 +#### 如何设置 Cookie + +* EasyHttp 是基于 OkHttp 封装的,而 OkHttp 本身就是支持设置 Cookie,所以用法和 OkHttp 是一样的 + +```java +OkHttpClient okHttpClient = new OkHttpClient.Builder() + .cookieJar(new XxxCookieJar()) + .build(); + +EasyConfig.with(okHttpClient) + .setXxx() + .into(); +``` + +#### 如何添加或者删除全局参数 + +* 添加全局请求参数 + +```java +EasyConfig.getInstance().addParam("key", "value"); +``` + +* 移除全局请求参数 + +```java +EasyConfig.getInstance().removeParam("key"); +``` + +* 添加全局请求头 ```java -// 添加全局请求参数 -EasyConfig.getInstance().addParam("token", "abc"); -// 添加全局请求头 -EasyConfig.getInstance().addHeader("token", "abc"); +EasyConfig.getInstance().addHeader("key", "value"); ``` -#### 如何定义全局的动态参数 +* 移除全局请求头 + +```java +EasyConfig.getInstance().removeHeader("key"); +``` + +#### 如何动态添加全局的参数或者请求头 ```java EasyConfig.getInstance().setInterceptor(new IRequestInterceptor() { @Override - public void interceptArguments(IRequestApi api, HttpParams params, HttpHeaders headers) { - headers.put("timestamp", String.valueOf(System.currentTimeMillis())); + public void interceptArguments(@NonNull HttpRequest httpRequest, @NonNull HttpParams params, @NonNull HttpHeaders headers) { + // 添加请求头 + headers.put("key", "value"); + // 添加参数 + params.put("key", "value"); } }); ``` @@ -571,12 +666,13 @@ EasyConfig.getInstance().setInterceptor(new IRequestInterceptor() { ```java public final class XxxApi implements IRequestApi { - + + @NonNull @Override public String getApi() { return "xxx/xxxx"; } - + @HttpIgnore private String token; } @@ -588,8 +684,6 @@ public final class XxxApi implements IRequestApi { IRequestServer server = EasyConfig.getInstance().getServer(); // 获取当前全局的服务器主机地址 String host = server.getHost(); -// 获取当前全局的服务器路径地址 -String path = server.getPath(); ``` #### 如何修改接口的服务器配置 @@ -599,15 +693,11 @@ String path = server.getPath(); ```java public class XxxServer implements IRequestServer { + @NonNull @Override public String getHost() { return "https://www.xxxxxxx.com/"; } - - @Override - public String getPath() { - return "api/"; - } } ``` @@ -622,6 +712,7 @@ EasyConfig.getInstance().setServer(new XxxServer()); ```java public final class XxxApi extends XxxServer implements IRequestApi { + @NonNull @Override public String getApi() { return "xxx/xxxx"; @@ -634,16 +725,13 @@ public final class XxxApi extends XxxServer implements IRequestApi { ```java public final class XxxApi implements IRequestServer, IRequestApi { + @NonNull @Override public String getHost() { return "https://www.xxxxxxx.com/"; } - @Override - public String getPath() { - return "api/"; - } - + @NonNull @Override public String getApi() { return "xxx/xxxx"; @@ -658,30 +746,22 @@ public final class XxxApi implements IRequestServer, IRequestApi { ```java public class TestServer implements IRequestServer { + @NonNull @Override public String getHost() { return "https://www.test.xxxxxxx.com/"; } - - @Override - public String getPath() { - return "api/"; - } } ``` ```java public class ReleaseServer implements IRequestServer { + @NonNull @Override public String getHost() { return "https://www.xxxxxxx.com/"; } - - @Override - public String getPath() { - return "api/"; - } } ``` @@ -702,6 +782,7 @@ EasyConfig.getInstance().setServer(server); ```java public class H5Server implements IRequestServer { + @NonNull @Override public String getHost() { IRequestServer server = EasyConfig.getInstance().getServer(); @@ -710,11 +791,6 @@ public class H5Server implements IRequestServer { } return "https://www.h5.xxxxxxx.com/"; } - - @Override - public String getPath() { - return "api/"; - } } ``` @@ -723,6 +799,7 @@ public class H5Server implements IRequestServer { ```java public final class UserAgreementApi extends H5Server implements IRequestApi { + @NonNull @Override public String getApi() { return "user/agreement"; @@ -737,19 +814,16 @@ public final class UserAgreementApi extends H5Server implements IRequestApi { ```java public class XxxServer implements IRequestServer { + @NonNull @Override public String getHost() { return "https://www.xxxxxxx.com/"; } + @NonNull @Override - public String getPath() { - return "api/"; - } - - @Override - public BodyType getType() { - return BodyType.FORM; + public IHttpPostBodyStrategy getBodyType() { + return RequestBodyType.FORM; } } ``` @@ -759,19 +833,16 @@ public class XxxServer implements IRequestServer { ```java public class XxxServer implements IRequestServer { + @NonNull @Override public String getHost() { return "https://www.xxxxxxx.com/"; } + @NonNull @Override - public String getPath() { - return "api/"; - } - - @Override - public BodyType getType() { - return BodyType.JSON; + public IHttpPostBodyStrategy getBodyType() { + return RequestBodyType.JSON; } } ``` @@ -779,16 +850,18 @@ public class XxxServer implements IRequestServer { * 当然也支持对某个接口进行单独配置 ```java -public final class XxxApi implements IRequestApi, IRequestType { +public final class XxxApi implements IRequestApi, IRequestBodyType { + @NonNull @Override public String getApi() { return "xxx/xxxx"; } + @NonNull @Override - public BodyType getType() { - return BodyType.JSON; + public IHttpPostBodyStrategy getBodyType() { + return RequestBodyType.JSON; } } ``` @@ -800,49 +873,69 @@ public final class XxxApi implements IRequestApi, IRequestType { | 多级参数 | 不支持 | 支持 | | 文件上传 | 支持 | 不支持 | -#### 如何对整个 Json 字符串进行加密 +#### 如何对接口进行加密或者解密 -* 这块的需求比较奇葩,但是搭配 OkHttp 拦截器仍然是可以实现的,这得益于 EasyHttp 良好的框架设计 +* 关于这个问题,其实可以利用框架中提供的 IRequestInterceptor 接口来实现,通过重写接口中的对应方法进行拦截,修改对象的内容从而达到加密的效果。 ```java -OkHttpClient okHttpClient = new OkHttpClient.Builder() - .addInterceptor(new Interceptor() { - @Override - public Response intercept(Chain chain) throws IOException { - Request request = chain.request(); - RequestBody body = request.body(); - if (body instanceof JsonBody) { - body = new JsonBody("假装加密了:" + ((JsonBody) body).getJson()); - Request.Builder builder = request.newBuilder(); - builder.method(request.method(), body); - request = builder.build(); - } - return chain.proceed(request); - } - }) - .build(); -``` +public interface IRequestInterceptor { + + /** + * 拦截参数 + * + * @param httpRequest 接口对象 + * @param params 请求参数 + * @param headers 请求头参数 + */ + default void interceptArguments(@NonNull HttpRequest httpRequest, @NonNull HttpParams params, @NonNull HttpHeaders headers) {} + + /** + * 拦截请求头 + * + * @param httpRequest 接口对象 + * @param request 请求头对象 + * @return 返回新的请求头 + */ + @NonNull + default Request interceptRequest(@NonNull HttpRequest httpRequest, @NonNull Request request) { + return request; + } -* 在 Application 初始化 EasyHttp 的时候配置进去 + /** + * 拦截器响应头 + * + * @param httpRequest 接口对象 + * @param response 响应头对象 + * @return 返回新的响应头 + */ + @NonNull + default Response interceptResponse(@NonNull HttpRequest httpRequest, @NonNull Response response) { + return response; + } +} +``` ```java +// 在框架初始化的时候设置拦截器 EasyConfig.with(okHttpClient) - //.setXxxx(Xxxx) - //.setXxxx(Xxxx) - //.setXxxx(Xxxx) + // 设置请求参数拦截器 + .setInterceptor(new XxxInterceptor()) .into(); ``` +* 如果你只想对某个接口进行加解密,可以让 Api 类单独实现 IRequestInterceptor 接口,这样它就不会走全局的配置。 + #### 如何忽略某个参数 ```java public final class XxxApi implements IRequestApi { - + + @NonNull @Override public String getApi() { return "xxx/xxxx"; } - + @HttpIgnore private String address; } @@ -850,29 +943,53 @@ public final class XxxApi implements IRequestApi { #### 如何传入请求头 +* 给字段加上 `@HttpHeader` 注解即可,则表示这个字段是一个请求头,如果没有加上此注解,则框架默认将字段作为请求参数 + ```java public final class XxxApi implements IRequestApi { - + + @NonNull @Override public String getApi() { return "xxx/xxxx"; } - + @HttpHeader private String time; } ``` +#### 如何传入动态的请求头 + +* 传入一个 `HashMap` 类型的字段,并在上面添加 `@HTTPHeader` 注解即可 + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "xxx/xxxx"; + } + + @HTTPHeader + private HashMap headers; +} +``` + #### 如何重命名参数或者请求头的名称 +* 给字段加上 `@HttpRename` 注解即可,则可以修改参数名的值,如果没有加上此注解,则框架默认使用字段名作为参数名 + ```java public final class XxxApi implements IRequestApi { - + + @NonNull @Override public String getApi() { return "xxx/xxxx"; } - + @HttpRename("k") private String keyword; } @@ -884,12 +1001,13 @@ public final class XxxApi implements IRequestApi { ```java public final class XxxApi implements IRequestApi { - + + @NonNull @Override public String getApi() { return "xxx/xxxx"; } - + private File file; } ``` @@ -898,12 +1016,13 @@ public final class XxxApi implements IRequestApi { ```java public final class XxxApi implements IRequestApi { - + + @NonNull @Override public String getApi() { return "xxx/xxxx"; } - + private InputStream inputStream; } ``` @@ -912,12 +1031,13 @@ public final class XxxApi implements IRequestApi { ```java public final class XxxApi implements IRequestApi { - + + @NonNull @Override public String getApi() { return "xxx/xxxx"; } - + private RequestBody requestBody; } ``` @@ -926,12 +1046,13 @@ public final class XxxApi implements IRequestApi { ```java public final class XxxApi implements IRequestApi { - + + @NonNull @Override public String getApi() { return "xxx/xxxx"; } - + private List files; } ``` @@ -962,16 +1083,18 @@ EasyConfig.with(builder.build()) * 局部配置(只在某个接口上生效) ```java -public final class XxxApi implements IRequestApi, IRequestClient { +public final class XxxApi implements IRequestApi, IRequestHttpClient { + @NonNull @Override public String getApi() { return "xxxx/"; } + @NonNull @Override - public OkHttpClient getClient() { - OkHttpClient.Builder builder = EasyConfig.getInstance().getClient().newBuilder(); + public OkHttpClient getOkHttpClient() { + OkHttpClient.Builder builder = EasyConfig.getInstance().getOkHttpClient().newBuilder(); builder.readTimeout(5000, TimeUnit.MILLISECONDS); builder.writeTimeout(5000, TimeUnit.MILLISECONDS); builder.connectTimeout(5000, TimeUnit.MILLISECONDS); @@ -986,15 +1109,33 @@ public final class XxxApi implements IRequestApi, IRequestClient { EasyConfig.getInstance().setLogEnabled(false); ``` +#### 如何修改日志打印策略 + +* 可以先定义一个类实现 [IHttpLogStrategy](library/src/main/java/com/hjq/http/config/IHttpLogStrategy.java) 接口,然后在框架初始化的时候传入即可 + +```java +EasyConfig.with(okHttpClient) + ....... + // 设置自定义的日志打印策略 + .setLogStrategy(new XxxStrategy()) + .into(); +``` + +* 需要修改日志打印策略的场景 + + * 需要将请求的日志写入到本地 + + * 需要修改打印的请求日志格式 + #### 如何取消已发起的请求 ```java -// 取消和这个 LifecycleOwner 关联的请求 -EasyHttp.cancel(LifecycleOwner lifecycleOwner); +// 根据 TAG 取消请求任务 +EasyHttp.cancelByTag(Object tag); // 取消指定 Tag 标记的请求 -EasyHttp.cancel(Object tag); +EasyHttp.cancelByTag(Object tag); // 取消所有请求 -EasyHttp.cancel(); +EasyHttp.cancelAll(); ``` #### 如何延迟发起一个请求 @@ -1004,51 +1145,24 @@ EasyHttp.post(MainActivity.this) .api(new XxxApi()) // 延迟 5 秒后请求 .delay(5000) - .request(new HttpCallback>(MainActivity.this) { + .request(new HttpCallbackProxy>(this) { @Override - public void onSucceed(HttpData result) { - + public void onHttpSuccess(@NonNull HttpData result) { + } }); ``` -#### 如何对请求头和响应头进行加解密 - -* 关于这个问题,其实可以利用框架中提供的 IRequestHandler 接口,在 requestStart 方法中对 Request 对象进行加密,而在 requestSucceed 方法中对 Response 对象解密,然后在 EasyHttp 初始化的时候将 IRequestHandler 实现对象传入给框架即可。 +#### 如何对接口路径进行动态化拼接 ```java -public interface IRequestHandler { +public final class XxxApi implements IRequestApi { - /** - * 请求开始 - */ - default Request requestStart(LifecycleOwner lifecycle, IRequestApi api, Request request) { - return request; - } - - /** - * 请求成功时回调 - */ - Object requestSucceed(LifecycleOwner lifecycle, IRequestApi api, Response response, Type type) throws Exception; - - /** - * 请求失败 - */ - Exception requestFail(LifecycleOwner lifecycle, IRequestApi api, Exception e); -} -``` - -* 如果你想对某个接口进行加解密,可以根据方法中的 api 参数对象来判断 ,如果你想对部分接口进行加解密,可以让外层的 IRequestApi 类实现统一的接口来标识这些接口,然后在 requestStart 和 requestFail 方法中判断 api 参数对象是否实现了这个接口来决定要不要进行加解密。 - -#### 如何对接口路径进行动态化拼接 - -```java -public final class XxxApi implements IRequestApi { - - @Override - public String getApi() { - return "article/query/" + pageNumber + "/json"; + @NonNull + @Override + public String getApi() { + return "article/query/" + pageNumber + "/json"; } @HttpIgnore @@ -1061,6 +1175,20 @@ public final class XxxApi implements IRequestApi { } ``` +#### 如何动态化整个请求的 url + +```java +EasyHttp.post(this) + .api(new RequestUrl("https://xxxx.com/aaaa")) + .request(new HttpCallbackProxy(this) { + + @Override + public void onHttpSuccess(@NonNull Xxx result) { + + } + }); +``` + #### Https 如何配置信任所有证书 * 在初始化 OkHttp 的时候这样设置 @@ -1093,10 +1221,10 @@ public final class HttpUrls { ```java EasyHttp.post(this) .api(HttpUrls.GET_USER_INFO) - .request(new HttpCallback>(this) { + .request(new HttpCallbackProxy>(this) { @Override - public void onSucceed(HttpData result) { + public void onHttpSuccess(@NonNull HttpData result) { } }); @@ -1108,122 +1236,369 @@ EasyHttp.post(this) #### 框架只能传入 LifecycleOwner 该怎么办 -* 其中 AndroidX.AppCompatActivity 和 AndroidX.Fragment 都是 LifecycleOwner 子类的,这个是毋庸置疑的 +* 其中 `androidx.appcompat.app.AppCompatActivity` 和 `androidx.fragment.app.Fragment` 都是 LifecycleOwner 子类的,这个是毋庸置疑的,可以直接当做 LifecycleOwner 传给框架 -* 但是你如果传入的是 Activity 对象,并非 AppCompatActivity 对象,那么你可以这样写 +* 但是你如果传入的是 `android.app.Activity` 对象,并非 `androidx.appcompat.app.AppCompatActivity` 对象,那么你可以这样写 ```java EasyHttp.post(new ActivityLifecycle(this)) .api(new XxxApi()) - .request(new HttpCallback>(this) { + .request(new HttpCallbackProxy>(this) { @Override - public void onSucceed(HttpData result) { + public void onHttpSuccess(@NonNull HttpData result) { } }); ``` -* 你如果想在 Service 中使用 EasyHttp,请将 Service 直接继承框架中的 LifecycleService 类,又或者在项目中封装一个带有 Lifecycle 特性的 Service 基类,具体实现如下: +* 如果你传入的是 `android.app.Fragment` 对象,并非 `androidx.fragment.app.Fragment` 对象,请将 Fragment 直接继承框架中的 LifecycleAppFragment 类,又或者在项目中封装一个带有 Lifecycle 特性的 Fragment 基类 + +* 你如果想在 `android.app.Service` 中使用 EasyHttp,请将 Service 直接继承框架中的 LifecycleService 类,又或者在项目中封装一个带有 Lifecycle 特性的 Service 基类 + +* 如果以上条件都不满足,但是你就是想在某个地方请求网络,那么你可以这样写 ```java -public abstract class LifecycleService extends Service implements LifecycleOwner { +EasyHttp.post(ApplicationLifecycle.getInstance()) + .api(new XxxApi()) + .tag("abc") + .request(new OnHttpListener>() { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + + @Override + public void onHttpFail(@NonNull Throwable throwable) { + + } + }); +``` + +* 需要注意的是,传入 ApplicationLifecycle 将意味着框架无法自动把控请求的生命周期,如果在 Application 中这样写是完全可以的,但是不能在 Activity 或者 Service 中这样写,因为这样可能会导致内存泄漏。 + +* 除了 Application,如果你在 Activity 或者 Service 中采用了 ApplicationLifecycle 的写法,那么为了避免内存泄漏或者崩溃的事情发生,需要你在请求的时候设置对应的 Tag,然后在恰当的时机手动取消请求(一般在 Activity 或者 Service 销毁或者退出的时候取消请求)。 + +```java +EasyHttp.cancelByTag("abc"); +``` + +#### 如何在 ViewModel 中使用 EasyHttp 请求网络 + +* 第一步:封装一个 BaseViewModel,并将 LifecycleOwner 特性植入进去 + +```java +public class BaseViewModel extends ViewModel implements LifecycleOwner { private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); + public BaseViewModel() { + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + } + + @Override + protected void onCleared() { + super.onCleared(); + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); + } + @NonNull @Override public Lifecycle getLifecycle() { return mLifecycle; } +} +``` - @Override - public void onCreate() { - super.onCreate(); - mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); - } +* 第二步:让业务 ViewModel 类继承至 BaseViewModel,具体案例如下 - @Override - public void onDestroy() { - super.onDestroy(); - mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); +```java +public class XxxViewModel extends BaseViewModel { + + public void xxxx() { + EasyHttp.post(this) + .api(new XxxApi()) + .request(new OnHttpListener>() { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + + @Override + public void onHttpFail(@NonNull Throwable throwable) { + + } + }); } } ``` -* 如果以上条件都不满足,但是你就是想在某个地方请求网络,那么你可以这样写 +#### 我想取消请求时显示的加载对话框该怎么办 + +* 首先这个加载对话框不是框架自带的,是可以修改或者取消的,主要有两种方式可供选择 + +* 第一种方式:重写 HttpCallbackProxy 类回调方法 ```java -EasyHttp.post(new ApplicationLifecycle()) +EasyHttp.post(this) .api(new XxxApi()) - .tag("abc") - .request(new OnHttpListener>() { + .request(new HttpCallbackProxy(this) { @Override - public void onSucceed(HttpData result) { + public void onHttpStart(@NonNull IRequestApi api) { + // 重写方法并注释父类调用 + //super.onHttpStart(call); + } + + @Override + public void onHttpEnd(@NonNull IRequestApi api) { + // 重写方法并注释父类调用 + //super.onHttpEnd(call); + } + }); +``` + +* 第二种方式:直接实现 OnHttpListener 接口 + +```java + +EasyHttp.post(this) + .api(new XxxApi()) + .request(new OnHttpListener() { + + @Override + public void onHttpSuccess(@NonNull Xxx result) { } @Override - public void onFail(Exception e) { + public void onHttpFail(@NonNull Throwable throwable) { } }); ``` -* 需要注意的是,传入 ApplicationLifecycle 将意味着框架无法自动把控请求的生命周期,如果在 Application 中这样写是完全可以的,但是不能在 Activity 或者 Service 中这样写,因为这样可能会导致内存泄漏。 +#### 我想用 Json 数组作为参数进行上传该怎么办 -* 除了 Application,如果你在 Activity 或者 Service 中采用了 ApplicationLifecycle 的写法,那么为了避免内存泄漏或者崩溃的事情发生,需要你在请求的时候设置对应的 Tag,然后在恰当的时机手动取消请求(一般在 Activity 或者 Service 销毁或者退出的时候取消请求)。 +* 由于 Api 类最终会转换成一个 JsonObject 类型的字符串,如果你需要上传 JsonArray 类型的字符串,请使用以下方式实现 ```java -EasyHttp.cancel("abc"); +List parameter = new ArrayList(); +list.add(xxx); +list.add(xxx); +String json = gson.toJson(parameter); + +EasyHttp.post(this) + .api(new XxxApi()) + .body(new JsonRequestBody(json)) + .request(new HttpCallbackProxy>(this) { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + }); ``` -#### 我想取消请求时显示的加载对话框该怎么办 +* 但是我个人不推荐将 JsonArray 作为参数的根部类型,因为这样的接口后续的扩展性极差。 -* 首先这个加载对话框不是框架自带的,是可以修改或者取消的,主要有两种方式可供选择 +#### 接口参数的 Key 值是动态变化的该怎么办 -* 第一种方式:重写 HttpCallback 类方法 +* 框架是通过反射解析 Api 类中的字段来作为参数的,字段名作为参数的 Key 值,字段值作为参数的 Value 值,由于 Java 无法动态更改类的字段名,所以无法通过正常的手段进行修改,你如果有这种需求,请通过以下方式进行实现 ```java +HashMap parameter = new HashMap(); + +// 添加全局参数 +HashMap globalParams = EasyConfig.getInstance().getParams(); +Set keySet = globalParams.keySet(); +for (String key : keySet) { + parameter.put(key, globalParams.get(key)); +} + +// 添加自定义参数 +parameter.put("key1", value1); +parameter.put("key2", value2); + +String json = gson.toJson(parameter); +JsonRequestBody jsonRequestBody = new JsonRequestBody(json) + EasyHttp.post(this) .api(new XxxApi()) - .request(new HttpCallback(this) { + .body(jsonRequestBody) + .request(new HttpCallbackProxy>(this) { @Override - public void onStart(Call call) { - // 重写方法并注释父类调用 - //super.onStart(call); + public void onHttpSuccess(@NonNull HttpData result) { + } + }); +``` + +#### 如何设置自定义的 UA 标识 + +* 首先 UA 是 User Agent 的简称,当我们没有设置自定义 UA 标识的时候,那么 OkHttp 会在 BridgeInterceptor 拦截器添加一个默认的 UA 标识,那么如何在 EasyHttp 设置自定义 UA 标识呢?其实很简单,UA 标识本质上其实就是一个请求头,在 EasyHttp 中添加一个请求头为 `User-Agent` 的参数即可,至于怎么添加请求头,前面的文档已经有介绍了,这里不再赘述。 + +#### 我想修改请求回调所在的线程该怎么办 + +```java +EasyHttp.post(this) + .api(new XxxApi()) + // 表示回调是在子线程中进行 + .schedulers(ThreadSchedulers.IO) + .request(new HttpCallbackProxy>(this) { @Override - public void onEnd(Call call) { - // 重写方法并注释父类调用 - //super.onEnd(call); + public void onHttpSuccess(@NonNull HttpData result) { + } }); ``` -* 第二种方式:直接实现 OnHttpListener 接口 +#### 我想自定义一个 RequestBody 进行请求该怎么办 -```java +* 在一些极端的情况下,框架无法满足使用的前提下,这个时候需要自定义 `RequestBody` 来实现,那么怎么使用自定义 `RequestBody` 呢?框架其实有开放方法,具体使用示例如下: +```java EasyHttp.post(this) .api(new XxxApi()) - .request(new OnHttpListener() { + .body(RequestBody body) + .request(new HttpCallbackProxy>(this) { @Override - public void onSucceed(Xxx result) { - + public void onHttpSuccess(@NonNull HttpData result) { + } + }); +``` + +* 需要注意的是:由于 Post 请求是将参数放置到 `RequestBody` 上面,而一个请求只能设置一个 `RequestBody`,如果你设置了自定义 `body(RequestBody body)`,那么框架将不会去将 `XxxApi` 类中的字段解析成参数。另外除了 Post 请求,Put 请求和 Patch 请求也可以使用这种方式进行设置,这里不再赘述。 + +#### 我想自定义请求头中的 ContentType 该怎么做 + +* 具体的写法示例如下: + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "xxx/xxx"; + } + + @HttpHeader + @HttpRename("Content-Type") + private String contentType = "application/x-www-form-urlencoded;charset=utf-8"; +} +``` + +* 需要注意的是:此功能仅是在框架 **11.5** 版本的时候加上的,之前的版本没有这一功能 + +#### 我想自定义 Get 请求参数中的 key 和 value 该怎么做 + +* 先自定义一个 Api 类,然后通过 `getApi` 方法将参数动态拼接上去 + +```java +public final class CustomParameterApi implements IRequestApi { + + @HttpIgnore + @NonNull + private final Map parameters; + + public CustomParameterApi() { + this(new HashMap()); + } + + public CustomParameterApi(@NonNull Map parameters) { + this.parameters = parameters; + } + + @NonNull + @Override + public String getApi() { + Set keys = parameters.keySet(); + + StringBuilder builder = new StringBuilder(); + int index = 0; + for (String key : keys) { + String value = parameters.get(key); + + if (index == 0) { + builder.append("?"); + } + builder.append(key) + .append("=") + .append(value); + if (index < keys.size() - 1) { + builder.append("&"); + } + index++; + } + + return "xxx/xxx" + builder; + } + + public CustomParameterApi putParameter(String key, String value) { + parameters.put(key, value); + return this; + } + + public CustomParameterApi removeParameter(String key) { + parameters.remove(key); + return this; + } +} +``` + +* 外层可以通过以下方式进行调用 + +```java +CustomParameterApi api = new CustomParameterApi(); +api.putParameter("key1", "value1"); +api.putParameter("key2", "value2"); + +EasyHttp.get(this) + .api(api) + .request(new HttpCallbackProxy(this) { @Override - public void onFail(Exception e) { + public void onHttpSuccess(@NonNull Xxx result) { } }); ``` +* 需要注意的是:这种实现方式仅适用于在框架设计无法满足需求的情况下,其他情况下作者并不提倡用这种方式,因为这样不方便管理请求参数的 key,还是推荐大家使用在类上面定义字段的方式来实现。 + +#### 我想在 Post 请求中定义类似 Get 请求参数该怎么做 + +* 直接拼接请求的参数到 url 上面,并且忽略某个字段的值(避免被解析成 Post 参数) + +```java +public final class XxxApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "article/query?pageNumber=" + pageNumber; + } + + @HttpIgnore + private int pageNumber; + + public XxxApi setPageNumber(int pageNumber) { + this.pageNumber = pageNumber; + return this; + } +} +``` + +* Ps:一般情况下我是不建议这样写的,这样的请求看起来不伦不类,即长得像一个 Get 请求,但是实际上却是一个 Post 请求。 + # 搭配 RxJava #### 准备工作 @@ -1247,16 +1622,19 @@ Observable.create(new ObservableOnSubscribe>() { @Override public void subscribe(ObservableEmitter> emitter) throws Exception { + HttpData data1; try { data1 = EasyHttp.post(MainActivity.this) .api(new SearchBlogsApi() .setKeyword("搬砖不再有")) .execute(new ResponseClass>() {}); - } catch (Exception e) { - e.printStackTrace(); - ToastUtils.show(e.getMessage()); - throw e; + } catch (Throwable throwable) { + if (throwable instanceof Exception) { + throw (Exception) throwable; + } else { + throw new RuntimeException(throwable); + } } HttpData data2; @@ -1265,10 +1643,12 @@ Observable.create(new ObservableOnSubscribe>() { .api(new SearchBlogsApi() .setKeyword(data1.getMessage())) .execute(new ResponseClass>() {}); - } catch (Exception e) { - e.printStackTrace(); - ToastUtils.show(e.getMessage()); - throw e; + } catch (Throwable throwable) { + if (throwable instanceof Exception) { + throw (Exception) throwable; + } else { + throw new RuntimeException(throwable); + } } emitter.onNext(data2); @@ -1283,58 +1663,22 @@ Observable.create(new ObservableOnSubscribe>() { @Override public void accept(HttpData data) throws Exception { - Log.d("EasyHttp", "最终结果为:" + data.getMessage()); + Log.i("EasyHttp", "最终结果为:" + data.getMessage()); } -}); -``` -#### 多个请求并行 - -```java -Observable.create(new ObservableOnSubscribe() { - - @Override - public void subscribe(ObservableEmitter emitter) throws Exception { - SearchBlogsApi api1 = new SearchBlogsApi() - .setKeyword("1"); - SearchBlogsApi api2 = new SearchBlogsApi() - .setKeyword("2"); - - emitter.onNext(api1); - emitter.onNext(api2); - emitter.onComplete(); - } -}) -.map(new Function>() { - - @Override - public HttpData apply(IRequestApi api) throws Exception { - try { - return EasyHttp.post(MainActivity.this) - .api(api) - .execute(new ResponseClass>() {}); - } catch (Exception e) { - e.printStackTrace(); - ToastUtils.show(e.getMessage()); - throw e; - } - } -}) -// 让被观察者执行在 IO 线程 -.subscribeOn(Schedulers.io()) -// 让观察者执行在主线程 -.observeOn(AndroidSchedulers.mainThread()) -.subscribe(new Consumer>() { +}, new Consumer() { @Override - public void accept(HttpData data) throws Exception { - Log.d("EasyHttp", "最终结果为:" + data.getMessage()); + public void accept(Throwable throwable) throws Exception { + toast(throwable.getMessage()); } }); ``` #### 发起轮询请求 +* 如果轮询的次数是有限,可以考虑使用 Http 请求来实现,但是如果轮询的次数是无限的,那么不推荐使用 Http 请求来实现,应当使用 WebSocket 来做,又或者其他长链接协议来做。 + ```java // 发起轮询请求,共发起三次请求,第一次请求在 5 秒后触发,剩下两次在 1 秒 和 2 秒后触发 Observable.intervalRange(1, 3, 5000, 1000, TimeUnit.MILLISECONDS) @@ -1348,10 +1692,10 @@ Observable.intervalRange(1, 3, 5000, 1000, TimeUnit.MILLISECONDS) EasyHttp.post(MainActivity.this) .api(new SearchBlogsApi() .setKeyword("搬砖不再有")) - .request(new HttpCallback>(MainActivity.this) { + .request(new HttpCallbackProxy>(MainActivity.this) { @Override - public void onSucceed(HttpData result) { + public void onHttpSuccess(@NonNull HttpData result) { } }); @@ -1369,18 +1713,18 @@ Observable.create(new ObservableOnSubscribe>() { EasyHttp.post(MainActivity.this) .api(new SearchBlogsApi() .setKeyword("搬砖不再有")) - .request(new HttpCallback>(MainActivity.this) { + .request(new HttpCallbackProxy>(MainActivity.this) { @Override - public void onSucceed(HttpData result) { + public void onHttpSuccess(@NonNull HttpData result) { emitter.onNext(result); emitter.onComplete(); } @Override - public void onFail(Exception e) { - super.onFail(e); - emitter.onError(e); + public void onHttpFail(@NonNull Throwable throwable) { + super.onHttpFail(throwable); + emitter.onError(throwable); } }); } @@ -1402,7 +1746,203 @@ Observable.create(new ObservableOnSubscribe>() { @Override public void accept(String s) throws Exception { - Log.d("EasyHttp", ""当前页码位置" + s); + Log.i("EasyHttp", "当前页码位置" + s); + } + +}, new Consumer() { + + @Override + public void accept(Throwable throwable) throws Exception { + toast(throwable.getMessage()); } }); +``` + +# 支持 Protobuf + +#### 准备工作 + +* 在项目根目录下得 `build.gradle` 文件加入以下配置 + +```groovy +buildscript { + + ...... + + dependencies { + // 自动生成 Protobuf 类插件:https://github.com/google/protobuf-gradle-plugin + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.9.3' + } +} +``` + +* 在项目 app 模块下的 `build.gradle` 文件中加入远程依赖 + +```groovy +...... + +apply plugin: 'com.google.protobuf' + +android { + ...... + + sourceSets { + main { + proto { + // 指定 Protobuf 文件路径 + srcDir 'src/main/proto' + } + } + } +} + +protobuf { + + protoc { + // 也可以配置本地编译器路径 + artifact = 'com.google.protobuf:protoc:3.23.0' + } + + generateProtoTasks { + all().each { task -> + task.builtins { + remove java + } + task.builtins { + // 生产java源码 + java {} + } + } + } +} + +dependencies { + + ...... + + // Protobuf:https://github.com/protocolbuffers/protobuf + implementation 'com.google.protobuf:protobuf-java:3.23.1' + implementation 'com.google.protobuf:protoc:3.23.0' +} +``` + +* 在 `app/src/main/` 新建一个名为 `proto` 文件夹,用于存放 Protobuf 相关文件,然后创建 `Person.proto` 文件,具体内容如下: + +```text +syntax = "proto3"; +package tutorial; + +message Person { + string name = 1; + int32 id = 2; + string email = 3; + string phone = 4; +} +``` + +* 然后 `Rebuild Project`,就能看到插件自动生成的 `PersonOuterClass` 类,`PersonOuterClass` 类中还有一个名为 `Person` 的静态内部类 + +#### 请求体解析成 Protobuf + +* 创建一个自定义的 RequestBody 类,用于将 Protocol 对象解析成流,建议存放在 `com.xxx.xxx/http/model` 包名下, + +```java +public class ProtocolRequestBody extends RequestBody { + + /** MessageLite 对象 */ + private final MessageLite mMessageLite; + /** 字节数组 */ + private final byte[] mBytes; + + public ProtocolRequestBody(MessageLite messageLite) { + mMessageLite = messageLite; + mBytes = messageLite.toByteArray(); + } + + @Override + public MediaType contentType() { + return ContentType.JSON; + } + + @Override + public long contentLength() { + // 需要注意:这里需要用字节数组的长度来计算 + return mBytes.length; + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + sink.write(mBytes, 0, mBytes.length); + } + + @NonNull + @Override + public String toString() { + return mMessageLite.toString(); + } + + /** + * 获取 MessageLite 对象 + */ + @NonNull + public MessageLite getMessageLite() { + return mMessageLite; + } +} +``` + +* 发起请求示例 + +```java +// 假装生成一个 Protobuf 对象 +Person person = Person.parseFrom("xxxxxxxxx".getBytes()); + +EasyHttp.post(this) + .api(new XxxApi()) + .body(new ProtocolRequestBody(person)) + .request(new HttpCallbackProxy>(this) { + + @Override + public void onHttpSuccess(@NonNull HttpData result) { + + } + }); +``` + +#### 响应体解析支持 Protobuf + +* 这个支持很简单了,只需要修改 `IRequestHandler` 接口的实现即可,具体的代码实现如下: + +``` +public final class RequestHandler implements IRequestHandler { + + ...... + + @NonNull + @Override + public Object requestSuccess(@NonNull HttpRequest httpRequest, @NonNull Response response, + @NonNull Type type) throws Throwable { + ...... + + final Object result; + + try { + if (type instanceof Class && AbstractParser.class.isAssignableFrom((Class) type)) { + String simpleName = ((Class) type).getSimpleName(); + Class clazz = Class.forName("tutorial." + simpleName + ".OuterClass." + simpleName); + Method parseFromMethod = clazz.getMethod("parseFrom", byte[].class); + // 调用静态方法 + result = parseFromMethod.invoke(null, (Object) text.getBytes()); + } else { + result = GsonFactory.getSingletonGson().fromJson(text, type); + } + } catch (JsonSyntaxException e) { + // 返回结果读取异常 + throw new DataException(mApplication.getString(R.string.http_data_explain_error), e); + } + + ...... + return result; + } +} ``` \ No newline at end of file diff --git a/README-en.md b/README-en.md new file mode 100644 index 0000000..e8f52c7 --- /dev/null +++ b/README-en.md @@ -0,0 +1,280 @@ +# [中文文档](README.md) + +# Simple and Easy-to-Use Network Framework + +* Project address: [Github](https://github.com/getActivity/EasyHttp) + +* Blog address: [Network Requests, As Elegant As Silk](https://www.jianshu.com/p/93cd59dec002) + +* [Click here to download demo apk directly](https://github.com/getActivity/EasyHttp/releases/download/13.5/EasyHttp.apk) + +![](picture/en/demo_preview.jpg) + +![](picture/en/resumable_transfer.gif) + +#### Integration Steps + +* If your project's Gradle configuration is `7.0 or below`, you need to add the following to your `build.gradle` file: + +```groovy +allprojects { + repositories { + // JitPack remote repository: https://jitpack.io + maven { url 'https://jitpack.io' } + } +} +``` + +* If your Gradle configuration is `7.0 or above`, you need to add the following to your `settings.gradle` file: + +```groovy +dependencyResolutionManagement { + repositories { + // JitPack remote repository: https://jitpack.io + maven { url 'https://jitpack.io' } + } +} +``` + +* After configuring the remote repository, add the remote dependency in the `build.gradle` file under the app module: + +```groovy +android { + // Support JDK 1.8 + compileOptions { + targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + // Network request framework: https://github.com/getActivity/EasyHttp + implementation 'com.github.getActivity:EasyHttp:13.5' + // OkHttp framework: https://github.com/square/okhttp + // noinspection GradleDependency + implementation 'com.squareup.okhttp3:okhttp:5.3.0' +} +``` + +* Note: Due to the use of Lifecycle features, the framework currently only supports integration with AndroidX projects. + +#### ProGuard Rules + +* Do not obfuscate Bean classes under a certain package: + +```text +# You must add this rule, otherwise fields in Bean classes may not be parsed correctly from backend responses. Replace xxx with the corresponding package name. +-keep class com.xxx.xxx.xxx.xxx.** { + ; +} +``` + +* The above ProGuard rules can be added to the `proguard-rules.pro` file in the main module. + +## [For detailed usage of the framework, please click here](HelpDoc.md) + +### Comparison of Different Network Request Frameworks + +| Feature or Detail | [EasyHttp](https://github.com/getActivity/EasyHttp) | [Retrofit](https://github.com/square/retrofit) | [OkGo](https://github.com/jeasonlzy/okhttp-OkGo) | +| :----: | :------: | :-----: | :-----: | +| Supported Version | 13.5 | 2.9.0 | 3.0.4 | +| Number of issues | [![](https://img.shields.io/github/issues/getActivity/EasyHttp.svg)](https://github.com/getActivity/EasyHttp/issues) | [![](https://img.shields.io/github/issues/square/retrofit.svg)](https://github.com/square/retrofit/issues) | [![](https://img.shields.io/github/issues/jeasonlzy/okhttp-OkGo.svg)](https://github.com/jeasonlzy/okhttp-OkGo/issues) | +| **aar package size** | 96 KB | 123 KB | 131 KB | +| minSdk requirement | API 14+ | API 21+ | API 14+ | +| Multi-domain configuration | ✅ | ❌ | ✅ | +| **Dynamic Host** | ✅ | ❌ | ❌ | +| Global parameters | ✅ | ❌ | ✅ | +| Log printing | ✅ | ❌ | ✅ | +| Timeout retry | ✅ | ✅ | ✅ | +| **Http cache configuration** | ✅ | ❌ | ✅ | +| **File download verification** | ✅ | ❌ | ❌ | +| **High-speed download** | ✅ | ❌ | ❌ | +| **Breakpoint resume download** | ✅ | ❌ | ✅ | +| Upload progress listener | ✅ | ❌ | ✅ | +| Json parameter submission | ✅ | ❌ | ✅ | +| Json log formatting | ✅ | ❌ | ❌ | +| **Request code positioning** | ✅ | ❌ | ❌ | +| **Delayed request initiation** | ✅ | ❌ | ❌ | +| **Partitioned storage adaptation** | ✅ | ❌ | ❌ | +| File upload types | File / FileContentResolver
InputStream / RequestBody | RequestBody | File | +| Batch file upload | ✅ | ❌ | ✅ | +| **Request lifecycle** | Automatic management | Needs wrapping | Needs wrapping | +| Parameter passing method | Field name + value | Parameter name + value | Custom Key + Value | +| Framework flexibility | High | Low | Medium | +| Framework learning cost | Medium | High | Low | +| **API memory cost** | Low | High | Low | +| **Interface maintenance cost** | Low | Medium | High | +| Framework maintenance status | Maintaining | Maintaining | Stopped maintenance | + +* In my opinion, Retrofit is not as easy to use, because many commonly used features are cumbersome to implement. For example, dynamic Host requires writing an interceptor, log printing requires writing an interceptor, and even adding global parameters requires writing an interceptor. One interceptor means writing a lot of code, and if not written carefully, it may introduce bugs, affecting the entire OkHttp request flow. I often wonder if these features can be done with a single line of code, because I think these are things that should be considered when designing a framework. This is my original intention for creating this framework. + +* OkGo also has some drawbacks. For example, it puts the parameter key references in the outer layer, which can cause some issues: + + 1. Key management issue: This key may be used many times in the outer layer, making key management uncontrollable. Subsequent interface changes may introduce risks of missing updates. Although this situation is rare, it cannot be ignored. EasyHttp does not have this problem because it does not place parameter keys in the outer layer. + + 2. Parameter annotation issue: From a code specification perspective, we should clarify the meaning and function of parameters in the code. If the key is placed in the outer layer, every place it is called needs to be annotated. EasyHttp uses field-based parameters, so you only need to annotate the field once. + + 3. Complete interface information display: When using OkGo for network requests, you can only see the parameters passed at the call site. For parameters referenced elsewhere, you can't see them directly and must trace the code or check the documentation. EasyHttp manages all information for an interface through a single class, which is equivalent to an interface document. + + 4. Dynamic configuration of interfaces: Besides parameters, an interface may also need to configure OkHttpClient objects, parameter submission methods, response handling, etc. OkGo can achieve this, but you have to write it every time. EasyHttp allows direct configuration in the API class, truly achieving one-time configuration. + +* EasyHttp adopts OOP thinking: a request represents an object, and dynamic configuration of interfaces is achieved through class inheritance and implementation, covering almost all functions needed in interface development. It is very simple and flexible to use. Retrofit uses annotation-based methods, which are very inflexible because annotations can only hold constants, limiting all parameters to be predefined, which is very unfavorable for dynamic configuration of interfaces. + +* Many people find writing an interface class troublesome. I have already thought of a good solution for this: you can write the Api class and Bean class together, so you don't need to write an extra class. Example: + +```java +public final class XxxApi implements IRequestApi { + + @Override + public String getApi() { + return "xxx/xxx"; + } + + private int xxx; + + public XxxApi setXxx(int xxx) { + this.xxx = xxx; + return this; + } + + ...... + + public final static class Bean { + + private int xyz; + + public int getXyz() { + return xyz; + } + + ...... + } +} +``` + +* Isn't it clever? This not only solves the problem well, but also encapsulates all information of an interface in this class, making it very intuitive and clear. + +#### Introduction to Automatic Lifecycle Management + +* The framework can automatically manage the lifecycle of requests, without the need for third-party wrappers or adaptation. This actually uses a Lifecycle feature in Jetpack. The framework binds network requests to the LifecycleOwner, and when the LifecycleOwner triggers destroy, the framework cancels the bound network requests. Compared to traditional methods, this is simpler and more convenient, and with Lifecycle support, it is more flexible. You don't need to care whether the request subject is an Activity, Fragment, or other object. + +* However, it is not without drawbacks. Since the Lifecycle feature is new in the AndroidX package, the current project must be based on the AndroidX library to integrate. + +* As they say, code is the best teacher. The implementation is as follows: + +```java +public final class HttpLifecycleManager implements LifecycleEventObserver { + + /** + * Bind the component's lifecycle + */ + public static void register(LifecycleOwner lifecycleOwner) { + lifecycleOwner.getLifecycle().addObserver(new HttpLifecycleManager()); + } + + @Override + public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { + if (event != Lifecycle.Event.ON_DESTROY) { + return; + } + + // Remove listener + source.getLifecycle().removeObserver(this); + // Cancel request + EasyHttp.cancelByTag(source); + } +} +``` + +#### Introduction to High-Speed Download Feature + +* In essence, this is similar to the principle of high-speed second transfer, except one is for upload and the other is for download. High-speed upload compares the MD5 value of the local file with that on the server. If the server has a file with the same MD5, it maps the file to the user's cloud drive, achieving high-speed transfer. High-speed download works similarly: it compares the MD5 value given by the backend with the local file. If the file exists and the MD5 matches, it means the file is the same as the one on the server, so it skips the download and directly calls the download success listener. + +* Both high-speed transfer and high-speed download use caching to achieve high speed. The difference is that one uses server-side cache and the other uses local cache. Both reduce server pressure and save user waiting time. + +#### Introduction to Code Positioning Feature + +* The framework outputs the code location of network requests in the log, so developers can directly click the Log to locate which class and line of code it is, greatly improving debugging efficiency, especially in complex business scenarios. I believe no one would refuse such a feature. + +![](picture/en/request_code.png) + +#### Introduction to Delayed Request Feature + +* We often have a need to initiate a network request, but want it to be delayed rather than immediate. EasyHttp considers and encapsulates this scenario. You can write code like this to achieve the effect: + +```java +EasyHttp.post(this) + .api(new XxxApi()) + .delay(3000) + .request(new HttpCallbackProxy>(this) { + + @Override + public void onHttpSuccess(HttpData result) { + + } + }); +``` + +* The delayed request feature supports both synchronous and asynchronous requests, as well as delayed download requests. + +#### Other Open Source Projects by the Author + +* Android middle office: [AndroidProject](https://github.com/getActivity/AndroidProject)![](https://img.shields.io/github/stars/getActivity/AndroidProject.svg)![](https://img.shields.io/github/forks/getActivity/AndroidProject.svg) + +* Android middle office kt version: [AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin)![](https://img.shields.io/github/stars/getActivity/AndroidProject-Kotlin.svg)![](https://img.shields.io/github/forks/getActivity/AndroidProject-Kotlin.svg) + +* Permissions framework: [XXPermissions](https://github.com/getActivity/XXPermissions) ![](https://img.shields.io/github/stars/getActivity/XXPermissions.svg) ![](https://img.shields.io/github/forks/getActivity/XXPermissions.svg) + +* Toast framework: [Toaster](https://github.com/getActivity/Toaster)![](https://img.shields.io/github/stars/getActivity/Toaster.svg)![](https://img.shields.io/github/forks/getActivity/Toaster.svg) + +* Title bar framework: [TitleBar](https://github.com/getActivity/TitleBar)![](https://img.shields.io/github/stars/getActivity/TitleBar.svg)![](https://img.shields.io/github/forks/getActivity/TitleBar.svg) + +* Floating window framework: [EasyWindow](https://github.com/getActivity/EasyWindow)![](https://img.shields.io/github/stars/getActivity/EasyWindow.svg)![](https://img.shields.io/github/forks/getActivity/EasyWindow.svg) + +* Device compatibility framework:[DeviceCompat](https://github.com/getActivity/DeviceCompat) ![](https://img.shields.io/github/stars/getActivity/DeviceCompat.svg) ![](https://img.shields.io/github/forks/getActivity/DeviceCompat.svg) + +* Shape view framework: [ShapeView](https://github.com/getActivity/ShapeView)![](https://img.shields.io/github/stars/getActivity/ShapeView.svg)![](https://img.shields.io/github/forks/getActivity/ShapeView.svg) + +* Shape drawable framework: [ShapeDrawable](https://github.com/getActivity/ShapeDrawable)![](https://img.shields.io/github/stars/getActivity/ShapeDrawable.svg)![](https://img.shields.io/github/forks/getActivity/ShapeDrawable.svg) + +* Language switching framework: [Multi Languages](https://github.com/getActivity/MultiLanguages)![](https://img.shields.io/github/stars/getActivity/MultiLanguages.svg)![](https://img.shields.io/github/forks/getActivity/MultiLanguages.svg) + +* Gson parsing fault tolerance: [GsonFactory](https://github.com/getActivity/GsonFactory)![](https://img.shields.io/github/stars/getActivity/GsonFactory.svg)![](https://img.shields.io/github/forks/getActivity/GsonFactory.svg) + +* Logcat viewing framework: [Logcat](https://github.com/getActivity/Logcat)![](https://img.shields.io/github/stars/getActivity/Logcat.svg)![](https://img.shields.io/github/forks/getActivity/Logcat.svg) + +* Nested scrolling layout framework:[NestedScrollLayout](https://github.com/getActivity/NestedScrollLayout) ![](https://img.shields.io/github/stars/getActivity/NestedScrollLayout.svg) ![](https://img.shields.io/github/forks/getActivity/NestedScrollLayout.svg) + +* Android version guide: [AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter)![](https://img.shields.io/github/stars/getActivity/AndroidVersionAdapter.svg)![](https://img.shields.io/github/forks/getActivity/AndroidVersionAdapter.svg) + +* Android code standard: [AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard)![](https://img.shields.io/github/stars/getActivity/AndroidCodeStandard.svg)![](https://img.shields.io/github/forks/getActivity/AndroidCodeStandard.svg) + +* Android resource summary:[AndroidIndex](https://github.com/getActivity/AndroidIndex) ![](https://img.shields.io/github/stars/getActivity/AndroidIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidIndex.svg) + +* Android open source leaderboard: [AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss)![](https://img.shields.io/github/stars/getActivity/AndroidGithubBoss.svg)![](https://img.shields.io/github/forks/getActivity/AndroidGithubBoss.svg) + +* Studio boutique plugins: [StudioPlugins](https://github.com/getActivity/StudioPlugins)![](https://img.shields.io/github/stars/getActivity/StudioPlugins.svg)![](https://img.shields.io/github/forks/getActivity/StudioPlugins.svg) + +* Emoji collection: [EmojiPackage](https://github.com/getActivity/EmojiPackage)![](https://img.shields.io/github/stars/getActivity/EmojiPackage.svg)![](https://img.shields.io/github/forks/getActivity/EmojiPackage.svg) + +* China provinces json: [ProvinceJson](https://github.com/getActivity/ProvinceJson)![](https://img.shields.io/github/stars/getActivity/ProvinceJson.svg)![](https://img.shields.io/github/forks/getActivity/ProvinceJson.svg) + +* Markdown documentation:[MarkdownDoc](https://github.com/getActivity/MarkdownDoc) ![](https://img.shields.io/github/stars/getActivity/MarkdownDoc.svg) ![](https://img.shields.io/github/forks/getActivity/MarkdownDoc.svg) + +## License + +```text +Copyright 2019 Huang JinQun + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` \ No newline at end of file diff --git a/README.md b/README.md index dcf487f..ad3d9d3 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ +# [English Doc](README-en.md) + # 简单易用的网络框架 -* 项目地址:[Github](https://github.com/getActivity/EasyHttp)、[码云](https://gitee.com/getActivity/EasyHttp) +* 项目地址:[Github](https://github.com/getActivity/EasyHttp) * 博客地址:[网络请求,如斯优雅](https://www.jianshu.com/p/93cd59dec002) -* [点击此处下载Demo](EasyHttp.apk) +* 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处下载Demo](https://github.com/getActivity/EasyHttp/releases/download/13.5/EasyHttp.apk) -![](EasyHttp.jpg) +![](picture/demo_code.png) * 另外对 OkHttp 原理感兴趣的同学推荐你看以下源码分析文章 @@ -22,24 +24,35 @@ * [OkHttp 精讲:CallServerInterceptor](https://www.jianshu.com/p/aa77af6251ff) +![](picture/zh/demo_preview.jpg) + +![](picture/zh/resumable_transfer.gif) + #### 集成步骤 -* 在项目根目录下的 `build.gradle` 文件中加入 +* 如果你的项目 Gradle 配置是在 `7.0 以下`,需要在 `build.gradle` 文件中加入 ```groovy -buildscript { +allprojects { repositories { + // JitPack 远程仓库:https://jitpack.io maven { url 'https://jitpack.io' } } } -allprojects { +``` + +* 如果你的 Gradle 配置是 `7.0 及以上`,则需要在 `settings.gradle` 文件中加入 + +```groovy +dependencyResolutionManagement { repositories { + // JitPack 远程仓库:https://jitpack.io maven { url 'https://jitpack.io' } } } ``` -* 在项目 app 模块下的 `build.gradle` 文件中加入 +* 配置完远程仓库后,在项目 app 模块下的 `build.gradle` 文件中加入远程依赖 ```groovy android { @@ -52,37 +65,55 @@ android { dependencies { // 网络请求框架:https://github.com/getActivity/EasyHttp - implementation 'com.github.getActivity:EasyHttp:10.0' + implementation 'com.github.getActivity:EasyHttp:13.5' // OkHttp 框架:https://github.com/square/okhttp // noinspection GradleDependency - implementation 'com.squareup.okhttp3:okhttp:3.12.13' + implementation 'com.squareup.okhttp3:okhttp:5.3.0' } ``` - + +* 需要注意的是:由于使用了 Lifecycle 特性,目前框架只支持 AndroidX 的项目集成 + +#### 混淆规则 + +* 不混淆某个包下的 Bean 类 + +```text +# 必须要加上此规则,否则可能会导致 Bean 类的字段无法解析成后台返回的字段,xxx 请替换成对应包名 +-keep class com.xxx.xxx.xxx.xxx.** { + ; +} +``` + +* 以上混淆规则,可以在主模块的 `proguard-rules.pro` 文件中加入 + ## [框架的具体用法请点击这里查看](HelpDoc.md) ### 不同网络请求框架之间的对比 | 功能或细节 | [EasyHttp](https://github.com/getActivity/EasyHttp) | [Retrofit](https://github.com/square/retrofit) | [OkGo](https://github.com/jeasonlzy/okhttp-OkGo) | | :----: | :------: | :-----: | :-----: | -| 对应版本 | 10.0 | 2.9.0 | 3.0.4 | +| 对应版本 | 13.5 | 2.9.0 | 3.0.4 | | issues 数 | [![](https://img.shields.io/github/issues/getActivity/EasyHttp.svg)](https://github.com/getActivity/EasyHttp/issues) | [![](https://img.shields.io/github/issues/square/retrofit.svg)](https://github.com/square/retrofit/issues) | [![](https://img.shields.io/github/issues/jeasonlzy/okhttp-OkGo.svg)](https://github.com/jeasonlzy/okhttp-OkGo/issues) | -| **aar 包大小** | 74 KB | 123 KB | 131 KB | +| **aar 包大小** | 96 KB | 123 KB | 131 KB | | minSdk 要求 | API 14+ | API 21+ | API 14+ | | 配置多域名 | ✅ | ❌ | ✅ | | **动态 Host** | ✅ | ❌ | ❌ | | 全局参数 | ✅ | ❌ | ✅ | | 日志打印 | ✅ | ❌ | ✅ | | 超时重试 | ✅ | ✅ | ✅ | -| **请求缓存** | ✅ | ❌ | ✅ | -| **下载校验** | ✅ | ❌ | ❌ | +| **配置 Http 缓存** | ✅ | ❌ | ✅ | +| **下载文件校验** | ✅ | ❌ | ❌ | | **极速下载** | ✅ | ❌ | ❌ | -| 批量上传文件 | ✅ | ❌ | ✅ | +| **下载断点续传** | ✅ | ❌ | ✅ | | 上传进度监听 | ✅ | ❌ | ✅ | | Json 参数提交 | ✅ | ❌ | ✅ | +| Json 日志打印格式化 | ✅ | ❌ | ❌ | | **请求代码定位** | ✅ | ❌ | ❌ | | **延迟发起请求** | ✅ | ❌ | ❌ | -| 上传文件类型 | File / InputStream / RequestBody | RequestBody | File | +| **分区存储适配** | ✅ | ❌ | ❌ | +| 上传文件类型 | File / FileContentResolver
InputStream / RequestBody | RequestBody | File | +| 批量上传文件 | ✅ | ❌ | ✅ | | **请求生命周期** | 自动管控 | 需要封装 | 需要封装 | | 参数传值方式 | 字段名 + 字段值 | 参数名 + 参数值 | 定义 Key + Value | | 框架灵活性 | 高 | 低 | 中 | @@ -96,18 +127,48 @@ dependencies { * OkGo 其实也存在一些弊端,例如会把参数的 key 引用放到外层去,这样会引发一些问题: 1. Key 管理问题:这个 key 可能会在外层被使用很多次,这样参数的 key 管理就会变得不可控,后续接口改动可能会出现漏改的风险,尽管这种情况比较少见,但是也不容忽视,而 EasyHttp 没有这个问题,因为 EasyHttp 不会将参数 key 值放置到外层中去。 - + 2. 接口参数注释的问题:站在代码的规范角度上讲,我们应该在代码中注明参数的含义及作用,如果一旦将 key 放到外层,那么每一处调用的地方都需要写一遍注释,而 EasyHttp 是将参数字段化,只需要写一次注释到字段上即可。 - + 3. 接口信息完整信息展示:使用 OkGo 请求网络,只能在调用的地方看到传递的接口参数,而一些被其他地方引用的参数,我们无法很直观的看到,只能通过追踪代码或者查看文档来得知,而 EasyHttp 将一个接口的信息全部通过一个类来管理的,这个类其实就相当于一个接口文档。 4. 接口的动态化配置:除了接口的参数之外,一个接口还有可能单独配置 OkHttpClient 对象、参数的提交方式、接口响应处理方式等,这些用 OkGo 是可以实现,但是每个地方都要写一次,而 EasyHttp 可以直接在 API 类中配置,真正做到一劳永逸。 * EasyHttp 采用了 OOP 思想,一个请求代表一个对象,通过类继承和实现的特性来对接口进行动态化配置,几乎涵盖接口开发中所有的功能,使用起来非常简单灵活。而 Retrofit 采用的是注解方式,缺点是灵活性极低,因为注解上面只能放常量,也就会限定你在注解上面的一切参数只能是事先定义好的,这对接口的动态化配置极不利的。 -* 有很多人觉得写一个接口类很麻烦,这个点确实有点麻烦,但是这块的付出是有收获的,从前期开发的效率考虑:OkGo> EasyHttp> Retrofit,但是从后期维护的效率考虑:EasyHttp> Retrofit> OkGo,之所以比较这三个框架,是因为框架的设计思想不同,但是我始终认为 EasyHttp 才是最好的设计,所以我创造了它。 +* 有很多人觉得写一个接口类很麻烦,关于这个问题我后面已经想到一个好方案了,大家可以将 Api 类和 Bean 类写在一起,这样大家就不需要多写一个类了,具体写法示例如下: + +```java +public final class XxxApi implements IRequestApi { + + @Override + public String getApi() { + return "xxx/xxx"; + } + + private int xxx; + + public XxxApi setXxx(int xxx) { + this.xxx = xxx; + return this; + } + + ...... + + public final static class Bean { + + private int xyz; + + public int getXyz() { + return xyz; + } -* 前期开发和后期维护哪个更重要?我觉得都重要,但是如果两者之间存在利益冲突,我会毫不犹豫地选择后期维护,因为前期开发占据的是小头,后期的持续维护才是大头。 + ...... + } +} +``` + +* 是不是很机智?这样不仅很好地解决了这一问题,还能将一个接口所有的信息都包裹在这个类中,非常直观,一览如云,妥妥的一箭双雕。 #### 生命周期自动管控介绍 @@ -123,7 +184,7 @@ public final class HttpLifecycleManager implements LifecycleEventObserver { /** * 绑定组件的生命周期 */ - public static void bind(LifecycleOwner lifecycleOwner) { + public static void register(LifecycleOwner lifecycleOwner) { lifecycleOwner.getLifecycle().addObserver(new HttpLifecycleManager()); } @@ -136,7 +197,7 @@ public final class HttpLifecycleManager implements LifecycleEventObserver { // 移除监听 source.getLifecycle().removeObserver(this); // 取消请求 - EasyHttp.cancel(source); + EasyHttp.cancelByTag(source); } } ``` @@ -149,9 +210,9 @@ public final class HttpLifecycleManager implements LifecycleEventObserver { #### 代码定位功能介绍 -* 框架会在日志打印中输出在网络请求的代码位置,这样开发者可以直接通过点击 Log 来定位代码是在哪个类哪行代码,这样可以极大提升我们排查问题的效率,特别是在请求一多且业务复杂的情况下,我相信没有一个人会拒绝这样的功能。 +* 框架会在日志打印中输出在网络请求的代码位置,这样开发者可以直接通过点击 Log 来定位是在哪个类哪行代码,这样可以极大提升我们排查问题的效率,特别是在请求一多且业务复杂的情况下,我相信没有一个人会拒绝这样的功能。 -![](RequestCode.png) +![](picture/zh/request_code.jpg) #### 延迟发起请求功能介绍 @@ -161,11 +222,11 @@ public final class HttpLifecycleManager implements LifecycleEventObserver { EasyHttp.post(this) .api(new XxxApi()) .delay(3000) - .request(new HttpCallback>(this) { + .request(new HttpCallbackProxy>(this) { @Override - public void onSucceed(HttpData result) { - + public void onHttpSuccess(HttpData result) { + } }); ``` @@ -174,36 +235,60 @@ EasyHttp.post(this) #### 作者的其他开源项目 -* 安卓技术中台:[AndroidProject](https://github.com/getActivity/AndroidProject) +* 安卓技术中台:[AndroidProject](https://github.com/getActivity/AndroidProject) ![](https://img.shields.io/github/stars/getActivity/AndroidProject.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidProject.svg) + +* 安卓技术中台 Kt 版:[AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin) ![](https://img.shields.io/github/stars/getActivity/AndroidProject-Kotlin.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidProject-Kotlin.svg) -* 权限框架:[XXPermissions](https://github.com/getActivity/XXPermissions) +* 权限框架:[XXPermissions](https://github.com/getActivity/XXPermissions) ![](https://img.shields.io/github/stars/getActivity/XXPermissions.svg) ![](https://img.shields.io/github/forks/getActivity/XXPermissions.svg) -* 吐司框架:[ToastUtils](https://github.com/getActivity/ToastUtils) +* 吐司框架:[Toaster](https://github.com/getActivity/Toaster) ![](https://img.shields.io/github/stars/getActivity/Toaster.svg) ![](https://img.shields.io/github/forks/getActivity/Toaster.svg) -* 标题栏框架:[TitleBar](https://github.com/getActivity/TitleBar) +* 标题栏框架:[TitleBar](https://github.com/getActivity/TitleBar) ![](https://img.shields.io/github/stars/getActivity/TitleBar.svg) ![](https://img.shields.io/github/forks/getActivity/TitleBar.svg) -* 国际化框架:[MultiLanguages](https://github.com/getActivity/MultiLanguages) +* 悬浮窗框架:[EasyWindow](https://github.com/getActivity/EasyWindow) ![](https://img.shields.io/github/stars/getActivity/EasyWindow.svg) ![](https://img.shields.io/github/forks/getActivity/EasyWindow.svg) -* 悬浮窗框架:[XToast](https://github.com/getActivity/XToast) +* 设备兼容框架:[DeviceCompat](https://github.com/getActivity/DeviceCompat) ![](https://img.shields.io/github/stars/getActivity/DeviceCompat.svg) ![](https://img.shields.io/github/forks/getActivity/DeviceCompat.svg) -* Shape 框架:[ShapeView](https://github.com/getActivity/ShapeView) +* ShapeView 框架:[ShapeView](https://github.com/getActivity/ShapeView) ![](https://img.shields.io/github/stars/getActivity/ShapeView.svg) ![](https://img.shields.io/github/forks/getActivity/ShapeView.svg) -* Gson 解析容错:[GsonFactory](https://github.com/getActivity/GsonFactory) +* ShapeDrawable 框架:[ShapeDrawable](https://github.com/getActivity/ShapeDrawable) ![](https://img.shields.io/github/stars/getActivity/ShapeDrawable.svg) ![](https://img.shields.io/github/forks/getActivity/ShapeDrawable.svg) -* 日志查看框架:[Logcat](https://github.com/getActivity/Logcat) +* 语种切换框架:[MultiLanguages](https://github.com/getActivity/MultiLanguages) ![](https://img.shields.io/github/stars/getActivity/MultiLanguages.svg) ![](https://img.shields.io/github/forks/getActivity/MultiLanguages.svg) + +* Gson 解析容错:[GsonFactory](https://github.com/getActivity/GsonFactory) ![](https://img.shields.io/github/stars/getActivity/GsonFactory.svg) ![](https://img.shields.io/github/forks/getActivity/GsonFactory.svg) + +* 日志查看框架:[Logcat](https://github.com/getActivity/Logcat) ![](https://img.shields.io/github/stars/getActivity/Logcat.svg) ![](https://img.shields.io/github/forks/getActivity/Logcat.svg) + +* 嵌套滚动布局框架:[NestedScrollLayout](https://github.com/getActivity/NestedScrollLayout) ![](https://img.shields.io/github/stars/getActivity/NestedScrollLayout.svg) ![](https://img.shields.io/github/forks/getActivity/NestedScrollLayout.svg) + +* Android 版本适配:[AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter) ![](https://img.shields.io/github/stars/getActivity/AndroidVersionAdapter.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidVersionAdapter.svg) + +* Android 代码规范:[AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard) ![](https://img.shields.io/github/stars/getActivity/AndroidCodeStandard.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidCodeStandard.svg) + +* Android 资源大汇总:[AndroidIndex](https://github.com/getActivity/AndroidIndex) ![](https://img.shields.io/github/stars/getActivity/AndroidIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidIndex.svg) + +* Android 开源排行榜:[AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss) ![](https://img.shields.io/github/stars/getActivity/AndroidGithubBoss.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidGithubBoss.svg) + +* Studio 精品插件:[StudioPlugins](https://github.com/getActivity/StudioPlugins) ![](https://img.shields.io/github/stars/getActivity/StudioPlugins.svg) ![](https://img.shields.io/github/forks/getActivity/StudioPlugins.svg) + +* 表情包大集合:[EmojiPackage](https://github.com/getActivity/EmojiPackage) ![](https://img.shields.io/github/stars/getActivity/EmojiPackage.svg) ![](https://img.shields.io/github/forks/getActivity/EmojiPackage.svg) + +* AI 资源大汇总:[AiIndex](https://github.com/getActivity/AiIndex) ![](https://img.shields.io/github/stars/getActivity/AiIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AiIndex.svg) + +* 省市区 Json 数据:[ProvinceJson](https://github.com/getActivity/ProvinceJson) ![](https://img.shields.io/github/stars/getActivity/ProvinceJson.svg) ![](https://img.shields.io/github/forks/getActivity/ProvinceJson.svg) + +* Markdown 语法文档:[MarkdownDoc](https://github.com/getActivity/MarkdownDoc) ![](https://img.shields.io/github/stars/getActivity/MarkdownDoc.svg) ![](https://img.shields.io/github/forks/getActivity/MarkdownDoc.svg) #### 微信公众号:Android轮子哥 ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/official_ccount.png) -#### Android 技术分享 QQ 群:78797078 +#### Android 技术 Q 群:10047167 -#### 如果您觉得我的开源库帮你节省了大量的开发时间,请扫描下方的二维码随意打赏,要是能打赏个 10.24 :monkey_face:就太:thumbsup:了。您的支持将鼓励我继续创作:octocat: +#### 如果您觉得我的开源库帮你节省了大量的开发时间,请扫描下方的二维码随意打赏,要是能打赏个 10.24 :monkey_face:就太:thumbsup:了。您的支持将鼓励我继续创作:octocat:([点击查看捐赠列表](https://github.com/getActivity/Donate)) ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png) ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_wechat.png) -#### [点击查看捐赠列表](https://github.com/getActivity/Donate) - #### 特别感谢 * [张鸿洋](https://github.com/hongyangAndroid) diff --git a/RequestCode.png b/RequestCode.png deleted file mode 100644 index 9433e13..0000000 Binary files a/RequestCode.png and /dev/null differ diff --git a/app/build.gradle b/app/build.gradle index 1bff2f2..28f621f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,18 +1,24 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from : '../common.gradle' android { - compileSdkVersion 30 - lintOptions { - abortOnError false + namespace 'com.hjq.easy.demo' + + buildFeatures { + // 是否生成 BuildConfig 类 + buildConfig true } defaultConfig { applicationId 'com.hjq.easy.demo' - minSdkVersion 16 - targetSdkVersion 30 - versionCode 1000 - versionName '10.0' + minSdk 23 + + // 仅保留 arm64-v8a 架构(需要注意的是 mmkv 库在 2.0 及之后的版本已经不支持在 32 位的机器上面运行) + ndk { + abiFilters 'arm64-v8a' + } } // 支持 JDK 1.8 @@ -45,9 +51,9 @@ android { } } - applicationVariants.all { variant -> + applicationVariants.configureEach { variant -> // apk 输出文件名配置 - variant.outputs.all { output -> + variant.outputs.configureEach { output -> outputFileName = rootProject.getName() + '.apk' } } @@ -55,36 +61,50 @@ android { dependencies { // 依赖 libs 目录下所有的 jar 和 aar 包 - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation fileTree(include: ['*.aar'], dir: 'libs') + implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') implementation project(':library') - // AppCompat 库:https://developer.android.google.cn/jetpack/androidx/releases/appcompat?hl=zh-cn - implementation 'androidx.appcompat:appcompat:1.3.0' + // AndroidX 库:https://github.com/androidx/androidx + implementation 'androidx.appcompat:appcompat:1.4.0' + // Material 库:https://github.com/material-components/material-components-android + implementation 'com.google.android.material:material:1.4.0' // OkHttp 框架:https://github.com/square/okhttp // 升级注意事项:https://www.jianshu.com/p/d12d0f536f55 // noinspection GradleDependency - implementation 'com.squareup.okhttp3:okhttp:3.12.13' + implementation 'com.squareup.okhttp3:okhttp:5.3.0' - // 吐司框架:https://github.com/getActivity/ToastUtils - implementation 'com.github.getActivity:ToastUtils:9.5' + // 吐司框架:https://github.com/getActivity/Toaster + implementation 'com.github.getActivity:Toaster:13.8' // 权限请求框架:https://github.com/getActivity/XXPermissions - implementation 'com.github.getActivity:XXPermissions:12.0' + implementation 'com.github.getActivity:XXPermissions:28.0' // 标题栏框架:https://github.com/getActivity/TitleBar - implementation 'com.github.getActivity:TitleBar:8.6' + implementation 'com.github.getActivity:TitleBar:10.8' - // Json 解析框架:https://github.com/google/gson - implementation 'com.google.code.gson:gson:2.8.8' // Gson 解析容错:https://github.com/getActivity/GsonFactory - implementation 'com.github.getActivity:GsonFactory:5.2' + implementation ('com.github.getActivity:GsonFactory:10.5') { + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-reflect' + } + // Json 解析框架:https://github.com/google/gson + implementation ('com.google.code.gson:gson:2.13.2') + // Kotlin 反射库:用于反射 Kotlin data class 类对象 + implementation 'org.jetbrains.kotlin:kotlin-reflect:2.2.21' + + // 腾讯 MMKV:https://github.com/Tencent/MMKV + implementation ('com.tencent:mmkv-static:2.3.0') { + // 避免版本不一致导致的依赖冲突,从而导致编译报错 + exclude group: 'androidx.annotation', module: 'annotation' + } + + // Bugly 异常捕捉:https://bugly.tds.qq.com/docs/sdk/android/ + implementation 'com.tencent.bugly_16kb:bugly-pro:4.4.7.3' // 日志调试框架:https://github.com/getActivity/Logcat - debugImplementation 'com.github.getActivity:Logcat:9.8' + debugImplementation 'com.github.getActivity:Logcat:12.3' - // 腾讯 MMKV:https://github.com/Tencent/MMKV - implementation 'com.tencent:mmkv-static:1.2.10' + // 内存泄漏监测框架:https://github.com/square/leakcanary + debugImplementation 'com.squareup.leakcanary:leakcanary-android:3.0-alpha-8' } \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index aeaf7fe..0b302a7 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -32,6 +32,16 @@ -dontwarn okhttp3.** -dontwarn okio.** +# EasyHttp +-keep class com.hjq.http.** {*;} +# 必须要加上此规则,否则会导致泛型解析失败 +-keep class * implements com.hjq.http.listener.OnHttpListener { + *; +} +-keep class * extends com.hjq.http.model.ResponseClass { + *; +} + # 不混淆这个包下的类 -keep class com.hjq.easy.demo.http.** { ; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 885d56a..9955881 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,57 +1,89 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/hjq/easy/demo/AppApplication.java b/app/src/main/java/com/hjq/easy/demo/AppApplication.java index 1d5afad..60cc581 100644 --- a/app/src/main/java/com/hjq/easy/demo/AppApplication.java +++ b/app/src/main/java/com/hjq/easy/demo/AppApplication.java @@ -1,19 +1,25 @@ package com.hjq.easy.demo; import android.app.Application; - +import androidx.annotation.NonNull; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonToken; +import com.hjq.easy.demo.http.model.HttpCacheStrategy; import com.hjq.easy.demo.http.model.RequestHandler; import com.hjq.easy.demo.http.server.ReleaseServer; import com.hjq.easy.demo.http.server.TestServer; +import com.hjq.gson.factory.GsonFactory; +import com.hjq.gson.factory.ParseExceptionCallback; import com.hjq.http.EasyConfig; -import com.hjq.http.config.IRequestApi; import com.hjq.http.config.IRequestInterceptor; import com.hjq.http.config.IRequestServer; import com.hjq.http.model.HttpHeaders; import com.hjq.http.model.HttpParams; -import com.hjq.toast.ToastUtils; +import com.hjq.http.request.HttpRequest; +import com.hjq.toast.Toaster; +import com.tencent.bugly.library.Bugly; +import com.tencent.bugly.library.BuglyBuilder; import com.tencent.mmkv.MMKV; - import okhttp3.OkHttpClient; /** @@ -27,9 +33,45 @@ public final class AppApplication extends Application { @Override public void onCreate() { super.onCreate(); - ToastUtils.init(this); + Toaster.init(this); + + // MMKV 初始化 MMKV.initialize(this); + // 初始化 Bugly 异常捕捉 + BuglyBuilder builder = new BuglyBuilder("8ca94a2408", "554da4f0-795b-4d69-ba8d-00aea7d6ca01"); + builder.debugMode = BuildConfig.DEBUG; + Bugly.init(this, builder); + + // 设置 Json 解析容错监听 + GsonFactory.setParseExceptionCallback(new ParseExceptionCallback() { + + @Override + public void onParseObjectException(TypeToken typeToken, String fieldName, JsonToken jsonToken) { + handlerGsonParseException("Object parsing exception: " + typeToken + "#" + fieldName + ", backend return type: " + jsonToken); + } + + @Override + public void onParseListItemException(TypeToken typeToken, String fieldName, JsonToken listItemJsonToken) { + handlerGsonParseException("List parsing exception: " + typeToken + "#" + fieldName + ", backend return item type: " + listItemJsonToken); + } + + @Override + public void onParseMapItemException(TypeToken typeToken, String fieldName, String mapItemKey, JsonToken mapItemJsonToken) { + handlerGsonParseException("Map parsing exception: " + typeToken + "#" + fieldName + ", mapItemKey = " + mapItemKey + ", backend return item type: " + mapItemJsonToken); + } + + private void handlerGsonParseException(String message) { + IllegalArgumentException exception = new IllegalArgumentException(message); + if (BuildConfig.DEBUG) { + throw exception; + } else { + // 上报到 Bugly 错误列表中 + Bugly.handleCatchException(Thread.currentThread(), exception, exception.getMessage(), null, true); + } + } + }); + // 网络请求框架初始化 IRequestServer server; if (BuildConfig.DEBUG) { @@ -44,14 +86,18 @@ public void onCreate() { EasyConfig.with(okHttpClient) // 是否打印日志 //.setLogEnabled(BuildConfig.DEBUG) - // 设置服务器配置 + // 设置服务器配置(必须设置) .setServer(server) - // 设置请求处理策略 + // 设置请求处理策略(必须设置) .setHandler(new RequestHandler(this)) + // 设置请求缓存实现策略(非必须) + .setCacheStrategy(new HttpCacheStrategy()) // 设置请求参数拦截器 .setInterceptor(new IRequestInterceptor() { @Override - public void interceptArguments(IRequestApi api, HttpParams params, HttpHeaders headers) { + public void interceptArguments(@NonNull HttpRequest httpRequest, + @NonNull HttpParams params, + @NonNull HttpHeaders headers) { headers.put("timestamp", String.valueOf(System.currentTimeMillis())); } }) diff --git a/app/src/main/java/com/hjq/easy/demo/BaseActivity.java b/app/src/main/java/com/hjq/easy/demo/BaseActivity.java index a4f68f7..14eed89 100644 --- a/app/src/main/java/com/hjq/easy/demo/BaseActivity.java +++ b/app/src/main/java/com/hjq/easy/demo/BaseActivity.java @@ -1,14 +1,11 @@ package com.hjq.easy.demo; import android.app.ProgressDialog; - +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; - -import com.hjq.easy.demo.http.model.HttpData; +import com.hjq.http.config.IRequestApi; import com.hjq.http.listener.OnHttpListener; -import com.hjq.toast.ToastUtils; - -import okhttp3.Call; +import com.hjq.toast.Toaster; /** * author : Android 轮子哥 @@ -36,7 +33,7 @@ public boolean isShowDialog() { public void showDialog() { if (mDialog == null) { mDialog = new ProgressDialog(this); - mDialog.setMessage(getResources().getString(R.string.http_loading)); + mDialog.setMessage(getResources().getString(R.string.dialog_loading_hint)); mDialog.setCancelable(false); mDialog.setCanceledOnTouchOutside(false); } @@ -61,24 +58,20 @@ public void hideDialog() { } @Override - public void onStart(Call call) { + public void onHttpStart(@NonNull IRequestApi api) { showDialog(); } @Override - public void onSucceed(Object result) { - if (result instanceof HttpData) { - ToastUtils.show(((HttpData) result).getMessage()); - } - } + public void onHttpSuccess(@NonNull Object result) {} @Override - public void onFail(Exception e) { - ToastUtils.show(e.getMessage()); + public void onHttpFail(@NonNull Throwable throwable) { + Toaster.show(throwable.getMessage()); } @Override - public void onEnd(Call call) { + public void onHttpEnd(@NonNull IRequestApi api) { hideDialog(); } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/easy/demo/MainActivity.java b/app/src/main/java/com/hjq/easy/demo/MainActivity.java index 20d2880..affa8d2 100644 --- a/app/src/main/java/com/hjq/easy/demo/MainActivity.java +++ b/app/src/main/java/com/hjq/easy/demo/MainActivity.java @@ -12,39 +12,39 @@ import android.os.Environment; import android.view.View; import android.widget.ProgressBar; - +import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; - +import com.hjq.bar.OnTitleBarListener; +import com.hjq.bar.TitleBar; import com.hjq.easy.demo.http.api.SearchAuthorApi; import com.hjq.easy.demo.http.api.SearchBlogsApi; import com.hjq.easy.demo.http.api.UpdateImageApi; import com.hjq.easy.demo.http.model.HttpData; import com.hjq.http.EasyHttp; -import com.hjq.http.listener.HttpCallback; +import com.hjq.http.EasyUtils; +import com.hjq.http.config.IRequestApi; +import com.hjq.http.listener.HttpCallbackProxy; import com.hjq.http.listener.OnDownloadListener; import com.hjq.http.listener.OnUpdateListener; +import com.hjq.http.model.FileContentResolver; import com.hjq.http.model.HttpMethod; import com.hjq.http.model.ResponseClass; -import com.hjq.permissions.OnPermissionCallback; -import com.hjq.permissions.Permission; import com.hjq.permissions.XXPermissions; -import com.hjq.toast.ToastUtils; - +import com.hjq.permissions.permission.PermissionLists; +import com.hjq.toast.Toaster; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.util.List; -import okhttp3.Call; - /** * author : Android 轮子哥 * github : https://github.com/getActivity/EasyHttp * time : 2019年05月19日 * desc : 网络请求示例 */ -public final class MainActivity extends BaseActivity implements View.OnClickListener, OnPermissionCallback { +public final class MainActivity extends BaseActivity implements View.OnClickListener { private ProgressBar mProgressBar; @@ -60,41 +60,43 @@ protected void onCreate(Bundle savedInstanceState) { findViewById(R.id.btn_main_exec).setOnClickListener(this); findViewById(R.id.btn_main_update).setOnClickListener(this); findViewById(R.id.btn_main_download).setOnClickListener(this); + + TitleBar titleBar = findViewById(R.id.tb_main_bar); + titleBar.setOnTitleBarListener(new OnTitleBarListener() { + @Override + public void onTitleClick(TitleBar titleBar) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(titleBar.getTitle().toString())); + startActivity(intent); + } + }); + requestPermission(); } private void requestPermission() { XXPermissions.with(this) - .permission(Permission.MANAGE_EXTERNAL_STORAGE) - .request(this); - } - - /** - * {@link OnPermissionCallback} - */ - - @Override - public void onGranted(List permissions, boolean all) { - - } - - @Override - public void onDenied(List permissions, boolean never) { - if (never) { - ToastUtils.show("授权失败,请手动授予存储权限"); - XXPermissions.startPermissionActivity(this, permissions); - } else { - ToastUtils.show("请先授予存储权限"); - requestPermission(); - } + .permission(PermissionLists.getManageExternalStoragePermission()) + .request((grantedList, deniedList) -> { + boolean allGranted = deniedList.isEmpty(); + if (!allGranted) { + // 判断请求失败的权限是否被用户勾选了不再询问的选项 + boolean doNotAskAgain = XXPermissions.isDoNotAskAgainPermissions(MainActivity.this, deniedList); + if (doNotAskAgain) { + Toaster.show(getString(R.string.toast_permission_storage)); + XXPermissions.startPermissionActivity(MainActivity.this, deniedList); + } else { + Toaster.show(getString(R.string.toast_permission_request)); + requestPermission(); + } + } + }); } @Override protected void onRestart() { super.onRestart(); - if (XXPermissions.isGranted(this, Permission.MANAGE_EXTERNAL_STORAGE)) { - onGranted(null, true); - } else { + if (!XXPermissions.isGrantedPermission(this, PermissionLists.getManageExternalStoragePermission())) { requestPermission(); } } @@ -107,24 +109,24 @@ public void onClick(View view) { EasyHttp.get(this) .api(new SearchAuthorApi() .setId(190000)) - .request(new HttpCallback>>(this) { + .request(new HttpCallbackProxy>>(this) { @Override - public void onSucceed(HttpData> result) { - ToastUtils.show("Get 请求成功,请看日志"); + public void onHttpSuccess(@NonNull HttpData> result) { + Toaster.show(getString(R.string.toast_get_success)); } }); } else if (viewId == R.id.btn_main_post) { EasyHttp.post(this) - .api(new SearchBlogsApi() - .setKeyword("搬砖不再有")) - .request(new HttpCallback>(this) { + .api(new SearchBlogsApi() + .setKeyword("搬砖不再有")) + .request(new HttpCallbackProxy>(MainActivity.this) { @Override - public void onSucceed(HttpData result) { - ToastUtils.show("Post 请求成功,请看日志"); + public void onHttpSuccess(@NonNull HttpData result) { + Toaster.show(getString(R.string.toast_post_success)); } }); @@ -138,10 +140,9 @@ public void onSucceed(HttpData result) { .api(new SearchBlogsApi() .setKeyword("搬砖不再有")) .execute(new ResponseClass>() {}); - ToastUtils.show("同步请求成功,请看日志"); - } catch (Exception e) { - e.printStackTrace(); - ToastUtils.show(e.getMessage()); + Toaster.show(getString(R.string.toast_sync_success)); + } catch (Throwable throwable) { + Toaster.show(throwable.getMessage()); } runOnUiThread(this::hideDialog); }).start(); @@ -149,14 +150,44 @@ public void onSucceed(HttpData result) { } else if (viewId == R.id.btn_main_update) { if (mProgressBar.getVisibility() == View.VISIBLE) { - ToastUtils.show("当前正在上传或者下载,请等待完成之后再进行操作"); + Toaster.show(getString(R.string.toast_upload_progress)); return; } - File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), getString(R.string.app_name) + ".png"); + /* + // 如果是放到外部存储目录下则需要适配分区存储 + String fileName = "EasyHttp.png"; + File file; + Uri outputUri; + if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.Q) { + // 适配 Android 10 分区存储特性 + ContentValues values = new ContentValues(); + // 设置显示的文件名 + values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName); + // 生成一个新的 uri 路径 + outputUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + file = new FileContentResolver(getContentResolver(), outputUri, fileName); + } else { + file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), fileName); + } + */ + + // 如果是放到外部存储的应用专属目录则不需要适配分区存储特性 + File file = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "test_image.png"); + if (!file.exists()) { // 生成图片到本地 - drawableToFile(ContextCompat.getDrawable(this, R.drawable.bg_material), file); + try { + Drawable drawable = ContextCompat.getDrawable(this, R.drawable.bg_material); + OutputStream outputStream = EasyUtils.openFileOutputStream(file); + if (((BitmapDrawable) drawable).getBitmap().compress(Bitmap.CompressFormat.PNG, 100, outputStream)) { + outputStream.flush(); + } + // 通知系统多媒体扫描该文件,否则会导致拍摄出来的图片或者视频没有及时显示到相册中,而需要通过重启手机才能看到 + MediaScannerConnection.scanFile(this, new String[]{file.getPath()}, null, null); + } catch (IOException e) { + e.printStackTrace(); + } } EasyHttp.post(this) @@ -164,28 +195,28 @@ public void onSucceed(HttpData result) { .request(new OnUpdateListener() { @Override - public void onStart(Call call) { + public void onUpdateStart(@NonNull IRequestApi api) { mProgressBar.setProgress(0); mProgressBar.setVisibility(View.VISIBLE); } @Override - public void onProgress(int progress) { + public void onUpdateProgressChange(int progress) { mProgressBar.setProgress(progress); } @Override - public void onSucceed(Void result) { - ToastUtils.show("上传成功"); + public void onUpdateSuccess(@NonNull Void result) { + Toaster.show(getString(R.string.toast_upload_success)); } @Override - public void onFail(Exception e) { - ToastUtils.show("上传失败"); + public void onUpdateFail(@NonNull Throwable throwable) { + Toaster.show(getString(R.string.toast_upload_fail)); } @Override - public void onEnd(Call call) { + public void onUpdateEnd(@NonNull IRequestApi api) { mProgressBar.setVisibility(View.GONE); } }); @@ -193,45 +224,72 @@ public void onEnd(Call call) { } else if (viewId == R.id.btn_main_download) { if (mProgressBar.getVisibility() == View.VISIBLE) { - ToastUtils.show("当前正在上传或者下载,请等待完成之后再进行操作"); + Toaster.show("当前正在上传或者下载,请等待完成之后再进行操作"); return; } + /* + // 如果是放到外部存储目录下则需要适配分区存储 + String fileName = "微信 8.0.15.apk"; + + File file; + Uri outputUri; + if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.Q) { + // 适配 Android 10 分区存储特性 + ContentValues values = new ContentValues(); + // 设置显示的文件名 + values.put(MediaStore.Downloads.DISPLAY_NAME, fileName); + // 生成一个新的 uri 路径 + // 注意这里使用 ContentResolver 插入的时候都会生成新的 Uri + // 解决方式将 ContentValues 和 Uri 作为 key 和 value 进行持久化关联 + // outputUri = getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values); + outputUri = ContentResolverUriStore.insert(this, Downloads.EXTERNAL_CONTENT_URI, values); + file = new FileContentResolver(getContentResolver(), outputUri, fileName); + } else { + file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName); + } + */ + + // 如果是放到外部存储的应用专属目录则不需要适配分区存储特性 + File file = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "wechat_8.0.15.apk"); + EasyHttp.download(this) .method(HttpMethod.GET) - .file(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "微信 7.0.14.apk")) + .file(file) //.url("https://qd.myapp.com/myapp/qqteam/AndroidQQ/mobileqq_android.apk") - .url("https://dldir1.qq.com/weixin/android/weixin7014android1660.apk") - .md5("6ec99cb762ffd9158e8b27dc33d9680d") + .url("https://dldir1.qq.com/weixin/android/weixin8015android2020_arm64.apk") + .md5("b05b25d4738ea31091dd9f80f4416469") + // 设置断点续传(默认不开启) + .resumableTransfer(true) .listener(new OnDownloadListener() { @Override - public void onStart(File file) { + public void onDownloadStart(@NonNull File file) { mProgressBar.setProgress(0); mProgressBar.setVisibility(View.VISIBLE); } @Override - public void onProgress(File file, int progress) { + public void onDownloadProgressChange(@NonNull File file, int progress) { mProgressBar.setProgress(progress); } @Override - public void onComplete(File file) { - ToastUtils.show("下载完成:" + file.getPath()); + public void onDownloadSuccess(@NonNull File file) { + Toaster.show(getString(R.string.toast_download_success, file.getPath())); installApk(MainActivity.this, file); } @Override - public void onError(File file, Exception e) { - ToastUtils.show("下载出错:" + e.getMessage()); + public void onDownloadFail(@NonNull File file, @NonNull Throwable throwable) { + Toaster.show(getString(R.string.toast_download_fail, throwable.getMessage())); + file.delete(); } @Override - public void onEnd(File file) { + public void onDownloadEnd(@NonNull File file) { mProgressBar.setVisibility(View.GONE); } - }) .start(); } @@ -241,73 +299,31 @@ public void onEnd(File file) { * 安装 Apk */ private void installApk(final Context context, final File file) { - XXPermissions.with(MainActivity.this) - // 安装包权限 - .permission(Permission.REQUEST_INSTALL_PACKAGES) - .request(new OnPermissionCallback() { - @Override - public void onGranted(List permissions, boolean all) { - if (all) { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - Uri uri; - if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.N) { - uri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - } else { - uri = Uri.fromFile(file); - } - - intent.setDataAndType(uri, "application/vnd.android.package-archive"); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - } - - @Override - public void onDenied(List permissions, boolean never) { - + XXPermissions.with(this) + .permission(PermissionLists.getRequestInstallPackagesPermission()) + .request((grantedList, deniedList) -> { + boolean allGranted = deniedList.isEmpty(); + if (!allGranted) { + Toaster.show(getString(R.string.toast_install_fail)); + return; + } + Intent intent = new Intent(Intent.ACTION_VIEW); + Uri uri; + if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.N) { + if (file instanceof FileContentResolver) { + uri = ((FileContentResolver) file).getContentUri(); + } else { + uri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file); } - }); - } - - /** - * 将 Drawable 写入到文件中 - */ - private void drawableToFile(Drawable drawable, File file) { - if (drawable == null) { - return; - } - - FileOutputStream outputStream = null; - - try { - if (file.exists()) { - file.delete(); - } - - if (!file.exists()) { - file.createNewFile(); - } - - outputStream = new FileOutputStream(file); - if (((BitmapDrawable) drawable).getBitmap().compress(Bitmap.CompressFormat.PNG, 100, outputStream)) { - outputStream.flush(); - } - - // 通知系统多媒体扫描该文件,否则会导致拍摄出来的图片或者视频没有及时显示到相册中,而需要通过重启手机才能看到 - MediaScannerConnection.scanFile(this, new String[]{file.getPath()}, null, null); - - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - if (outputStream != null) { - outputStream.close(); + } else { + uri = Uri.fromFile(file); } - } catch (IOException e) { - e.printStackTrace(); - } - } + + intent.setDataAndType(uri, "application/vnd.android.package-archive"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 对目标应用临时授权该 Uri 读写权限 + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + context.startActivity(intent); + }); } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/easy/demo/http/api/SearchAuthorApi.java b/app/src/main/java/com/hjq/easy/demo/http/api/SearchAuthorApi.java index a3bc2eb..11bf72f 100644 --- a/app/src/main/java/com/hjq/easy/demo/http/api/SearchAuthorApi.java +++ b/app/src/main/java/com/hjq/easy/demo/http/api/SearchAuthorApi.java @@ -1,5 +1,7 @@ package com.hjq.easy.demo.http.api; +import androidx.annotation.NonNull; + import com.hjq.http.config.IRequestApi; /** @@ -10,6 +12,7 @@ */ public final class SearchAuthorApi implements IRequestApi { + @NonNull @Override public String getApi() { return "wxarticle/chapters/json"; @@ -23,7 +26,7 @@ public SearchAuthorApi setId(int id) { return this; } - public final static class Bean { + public static final class Bean { private int courseId; private int id; diff --git a/app/src/main/java/com/hjq/easy/demo/http/api/SearchBlogsApi.java b/app/src/main/java/com/hjq/easy/demo/http/api/SearchBlogsApi.java index b244311..c496cc4 100644 --- a/app/src/main/java/com/hjq/easy/demo/http/api/SearchBlogsApi.java +++ b/app/src/main/java/com/hjq/easy/demo/http/api/SearchBlogsApi.java @@ -1,8 +1,8 @@ package com.hjq.easy.demo.http.api; +import androidx.annotation.NonNull; import com.hjq.http.annotation.HttpRename; import com.hjq.http.config.IRequestApi; - import java.util.List; /** @@ -13,6 +13,7 @@ */ public final class SearchBlogsApi implements IRequestApi { + @NonNull @Override public String getApi() { return "article/query/0/json"; @@ -27,7 +28,7 @@ public SearchBlogsApi setKeyword(String keyword) { return this; } - public final static class Bean { + public static final class Bean { private int curPage; private int offset; diff --git a/app/src/main/java/com/hjq/easy/demo/http/api/UpdateImageApi.java b/app/src/main/java/com/hjq/easy/demo/http/api/UpdateImageApi.java index 8d1c815..80e3a91 100644 --- a/app/src/main/java/com/hjq/easy/demo/http/api/UpdateImageApi.java +++ b/app/src/main/java/com/hjq/easy/demo/http/api/UpdateImageApi.java @@ -1,8 +1,8 @@ package com.hjq.easy.demo.http.api; +import androidx.annotation.NonNull; import com.hjq.http.config.IRequestApi; import com.hjq.http.config.IRequestServer; - import java.io.File; /** @@ -13,11 +13,13 @@ */ public final class UpdateImageApi implements IRequestServer, IRequestApi { + @NonNull @Override public String getHost() { return "https://graph.baidu.com/"; } + @NonNull @Override public String getApi() { return "upload/"; diff --git a/app/src/main/java/com/hjq/easy/demo/http/exception/ResultException.java b/app/src/main/java/com/hjq/easy/demo/http/exception/ResultException.java new file mode 100644 index 0000000..7ec88d8 --- /dev/null +++ b/app/src/main/java/com/hjq/easy/demo/http/exception/ResultException.java @@ -0,0 +1,32 @@ +package com.hjq.easy.demo.http.exception; + +import androidx.annotation.NonNull; + +import com.hjq.easy.demo.http.model.HttpData; +import com.hjq.http.exception.HttpException; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2019年06月25日 + * desc : 返回结果异常 + */ +public final class ResultException extends HttpException { + + private final HttpData mData; + + public ResultException(String message, HttpData data) { + super(message); + mData = data; + } + + public ResultException(String message, Throwable cause, HttpData data) { + super(message, cause); + mData = data; + } + + @NonNull + public HttpData getHttpData() { + return mData; + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/http/exception/TokenException.java b/app/src/main/java/com/hjq/easy/demo/http/exception/TokenException.java similarity index 81% rename from library/src/main/java/com/hjq/http/exception/TokenException.java rename to app/src/main/java/com/hjq/easy/demo/http/exception/TokenException.java index 14bdfc2..8b54ba1 100644 --- a/library/src/main/java/com/hjq/http/exception/TokenException.java +++ b/app/src/main/java/com/hjq/easy/demo/http/exception/TokenException.java @@ -1,18 +1,20 @@ -package com.hjq.http.exception; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/EasyHttp - * time : 2019年05月19日 - * desc : Token 失效异常 - */ -public final class TokenException extends HttpException { - - public TokenException(String message) { - super(message); - } - - public TokenException(String message, Throwable cause) { - super(message, cause); - } +package com.hjq.easy.demo.http.exception; + +import com.hjq.http.exception.HttpException; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2019年05月19日 + * desc : Token 失效异常 + */ +public final class TokenException extends HttpException { + + public TokenException(String message) { + super(message); + } + + public TokenException(String message, Throwable cause) { + super(message, cause); + } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/easy/demo/http/model/HttpCacheManager.java b/app/src/main/java/com/hjq/easy/demo/http/model/HttpCacheManager.java new file mode 100644 index 0000000..6502493 --- /dev/null +++ b/app/src/main/java/com/hjq/easy/demo/http/model/HttpCacheManager.java @@ -0,0 +1,95 @@ +package com.hjq.easy.demo.http.model; + +import androidx.annotation.NonNull; +import com.hjq.gson.factory.GsonFactory; +import com.hjq.http.config.IRequestApi; +import com.hjq.http.request.HttpRequest; +import com.tencent.mmkv.MMKV; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2022年03月22日 + * desc : Http 缓存管理器 + */ +public final class HttpCacheManager { + + private static final MMKV HTTP_CACHE_CONTENT = MMKV.mmkvWithID("http_cache_content");; + + private static final MMKV HTTP_CACHE_TIME = MMKV.mmkvWithID("http_cache_time"); + + /** + * 生成缓存的 key + */ + @NonNull + public static String generateCacheKey(@NonNull HttpRequest httpRequest) { + IRequestApi requestApi = httpRequest.getRequestApi(); + return "请替换成当前的用户 id" + "\n" + requestApi.getApi() + "\n" + GsonFactory.getSingletonGson().toJson(requestApi); + } + + /** + * 读取缓存 + */ + public static String readHttpCache(@NonNull String cacheKey) { + String cacheValue = HTTP_CACHE_CONTENT.getString(cacheKey, null); + if (cacheValue == null || cacheValue.isEmpty() || "{}".equals(cacheValue)) { + return null; + } + return cacheValue; + } + + /** + * 写入缓存 + */ + public static boolean writeHttpCache(String cacheKey, String cacheValue) { + return HTTP_CACHE_CONTENT.putString(cacheKey, cacheValue).commit(); + } + + /** + * 删除缓存 + */ + public static boolean deleteHttpCache(String cacheKey) { + return HTTP_CACHE_CONTENT.remove(cacheKey).commit(); + } + + /** + * 清理缓存 + */ + public static void clearCache() { + HTTP_CACHE_CONTENT.clearMemoryCache(); + HTTP_CACHE_CONTENT.clearAll(); + + HTTP_CACHE_TIME.clearMemoryCache(); + HTTP_CACHE_TIME.clearAll(); + } + + /** + * 获取 Http 写入缓存的时间 + */ + public static long getHttpCacheTime(String cacheKey) { + return HTTP_CACHE_TIME.getLong(cacheKey, 0); + } + + /** + * 设置 Http 写入缓存的时间 + */ + public static boolean setHttpCacheTime(String cacheKey, long cacheTime) { + return HTTP_CACHE_TIME.putLong(cacheKey, cacheTime).commit(); + } + + /** + * 判断缓存是否过期 + */ + public static boolean isCacheInvalidate(String cacheKey, long maxCacheTime) { + if (maxCacheTime == Long.MAX_VALUE) { + // 表示缓存长期有效,永远不会过期 + return false; + } + long httpCacheTime = getHttpCacheTime(cacheKey); + if (httpCacheTime == 0) { + // 表示不知道缓存的时间,这里默认当做已经过期了 + return true; + } + return httpCacheTime + maxCacheTime < System.currentTimeMillis(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/easy/demo/http/model/HttpCacheStrategy.java b/app/src/main/java/com/hjq/easy/demo/http/model/HttpCacheStrategy.java new file mode 100644 index 0000000..b1fbaa0 --- /dev/null +++ b/app/src/main/java/com/hjq/easy/demo/http/model/HttpCacheStrategy.java @@ -0,0 +1,74 @@ +package com.hjq.easy.demo.http.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.hjq.gson.factory.GsonFactory; +import com.hjq.http.EasyLog; +import com.hjq.http.config.IHttpCacheStrategy; +import com.hjq.http.request.HttpRequest; +import java.lang.reflect.Type; +import okhttp3.Response; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2025/03/23 + * desc : 请求缓存策略实现类 + */ +public final class HttpCacheStrategy implements IHttpCacheStrategy { + + @Nullable + @Override + public Object readCache(@NonNull HttpRequest httpRequest, @NonNull Type type, long cacheTime) { + String cacheKey = HttpCacheManager.generateCacheKey(httpRequest); + String cacheValue = HttpCacheManager.readHttpCache(cacheKey); + if (cacheValue == null || cacheValue.isEmpty() || "{}".equals(cacheValue)) { + return null; + } + EasyLog.printLog(httpRequest, "----- read cache key -----"); + EasyLog.printJson(httpRequest, cacheKey); + EasyLog.printLog(httpRequest, "----- read cache value -----"); + EasyLog.printJson(httpRequest, cacheValue); + EasyLog.printLog(httpRequest, "cacheTime = " + cacheTime); + boolean cacheInvalidate = HttpCacheManager.isCacheInvalidate(cacheKey, cacheTime); + EasyLog.printLog(httpRequest, "cacheInvalidate = " + cacheInvalidate); + if (cacheInvalidate) { + // 表示缓存已经过期了,直接返回 null 给外层,表示缓存不可用 + return null; + } + return GsonFactory.getSingletonGson().fromJson(cacheValue, type); + } + + @Override + public boolean writeCache(@NonNull HttpRequest httpRequest, @NonNull Response response, @NonNull Object result) { + String cacheKey = HttpCacheManager.generateCacheKey(httpRequest); + String cacheValue = GsonFactory.getSingletonGson().toJson(result); + if (cacheValue == null || cacheValue.isEmpty() || "{}".equals(cacheValue)) { + return false; + } + EasyLog.printLog(httpRequest, "----- write cache key -----"); + EasyLog.printJson(httpRequest, cacheKey); + EasyLog.printLog(httpRequest, "----- write cache value -----"); + EasyLog.printJson(httpRequest, cacheValue); + boolean writeHttpCacheResult = HttpCacheManager.writeHttpCache(cacheKey, cacheValue); + EasyLog.printLog(httpRequest, "writeHttpCacheResult = " + writeHttpCacheResult); + boolean refreshHttpCacheTimeResult = HttpCacheManager.setHttpCacheTime(cacheKey, System.currentTimeMillis()); + EasyLog.printLog(httpRequest, "refreshHttpCacheTimeResult = " + refreshHttpCacheTimeResult); + return writeHttpCacheResult && refreshHttpCacheTimeResult; + } + + @Override + public boolean deleteCache(@NonNull HttpRequest httpRequest) { + String cacheKey = HttpCacheManager.generateCacheKey(httpRequest); + EasyLog.printLog(httpRequest, "----- delete cache key -----"); + EasyLog.printJson(httpRequest, cacheKey); + boolean deleteHttpCacheResult = HttpCacheManager.deleteHttpCache(cacheKey); + EasyLog.printLog(httpRequest, "deleteHttpCacheResult = " + deleteHttpCacheResult); + return deleteHttpCacheResult; + } + + @Override + public void clearCache() { + HttpCacheManager.clearCache(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/easy/demo/http/model/HttpData.java b/app/src/main/java/com/hjq/easy/demo/http/model/HttpData.java index f8ba997..70ffd8c 100644 --- a/app/src/main/java/com/hjq/easy/demo/http/model/HttpData.java +++ b/app/src/main/java/com/hjq/easy/demo/http/model/HttpData.java @@ -1,5 +1,8 @@ package com.hjq.easy.demo.http.model; +import androidx.annotation.Nullable; +import java.util.Map; + /** * author : Android 轮子哥 * github : https://github.com/getActivity/EasyHttp @@ -8,13 +11,27 @@ */ public class HttpData { + /** 响应头 */ + @Nullable + private Map responseHeaders; + /** 返回码 */ private int errorCode; /** 提示语 */ private String errorMsg; /** 数据 */ + @Nullable private T data; + public void setResponseHeaders(@Nullable Map responseHeaders) { + this.responseHeaders = responseHeaders; + } + + @Nullable + public Map getResponseHeaders() { + return responseHeaders; + } + public int getCode() { return errorCode; } @@ -23,6 +40,7 @@ public String getMessage() { return errorMsg; } + @Nullable public T getData() { return data; } @@ -30,14 +48,22 @@ public T getData() { /** * 是否请求成功 */ - public boolean isRequestSucceed() { + public boolean isRequestSuccess() { + // 这里为了兼容 WanAndroid 接口才这样写,但是一般情况下不建议这么设计 + // 因为 int 的默认值就是 0,这样就会导致,后台返回结果码为 0 和没有返回的效果是一样的 + // 本质上其实不一样,没有返回结果码本身就是一种错误数据结构,理论上应该走失败的回调 + // 因为这里会判断是否等于 0,所以就会导致原本走失败的回调,结果走了成功的回调 + // 所以在定义错误码协议的时候,请不要将后台返回的某个成功码或者失败码的值设计成 0 + // 如果你的项目已经出现了这种情况,可以尝试将结果码的数据类型从 int 修改成 Integer + // 这样就可以通过结果码是否等于 null 来判断后台是否返回了,当然这样也有一些弊端 + // 后面外层在使用这个结果码的时候,要先对 Integer 对象进行一次判空,否则会出现空指针异常 return errorCode == 0; } /** * 是否 Token 失效 */ - public boolean isTokenFailure() { + public boolean isTokenInvalidation() { return errorCode == 1001; } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/easy/demo/http/model/RequestHandler.java b/app/src/main/java/com/hjq/easy/demo/http/model/RequestHandler.java index 20db654..d5a7554 100644 --- a/app/src/main/java/com/hjq/easy/demo/http/model/RequestHandler.java +++ b/app/src/main/java/com/hjq/easy/demo/http/model/RequestHandler.java @@ -2,38 +2,36 @@ import android.app.Application; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.ConnectivityManager; import android.net.NetworkInfo; - -import androidx.lifecycle.LifecycleOwner; - +import androidx.annotation.NonNull; import com.google.gson.JsonSyntaxException; import com.hjq.easy.demo.R; +import com.hjq.easy.demo.http.exception.ResultException; +import com.hjq.easy.demo.http.exception.TokenException; import com.hjq.gson.factory.GsonFactory; import com.hjq.http.EasyLog; -import com.hjq.http.config.IRequestApi; import com.hjq.http.config.IRequestHandler; import com.hjq.http.exception.CancelException; import com.hjq.http.exception.DataException; +import com.hjq.http.exception.FileMd5Exception; import com.hjq.http.exception.HttpException; import com.hjq.http.exception.NetworkException; +import com.hjq.http.exception.NullBodyException; import com.hjq.http.exception.ResponseException; -import com.hjq.http.exception.ResultException; import com.hjq.http.exception.ServerException; import com.hjq.http.exception.TimeoutException; -import com.hjq.http.exception.TokenException; -import com.tencent.mmkv.MMKV; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - +import com.hjq.http.request.HttpRequest; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.GenericArrayType; import java.lang.reflect.Type; import java.net.SocketTimeoutException; import java.net.UnknownHostException; - +import java.util.HashMap; +import java.util.Map; import okhttp3.Headers; import okhttp3.Response; import okhttp3.ResponseBody; @@ -47,22 +45,21 @@ public final class RequestHandler implements IRequestHandler { private final Application mApplication; - private final MMKV mMmkv; public RequestHandler(Application application) { mApplication = application; - mMmkv = MMKV.mmkvWithID("http_cache_id"); } + @NonNull @Override - public Object requestSucceed(LifecycleOwner lifecycle, IRequestApi api, Response response, Type type) throws Exception { + public Object requestSuccess(@NonNull HttpRequest httpRequest, @NonNull Response response, @NonNull Type type) throws Throwable { if (Response.class.equals(type)) { return response; } if (!response.isSuccessful()) { - // 返回响应异常 - throw new ResponseException(mApplication.getString(R.string.http_response_error) + ",responseCode:" + response.code() + ",message:" + response.message(), response); + throw new ResponseException(String.format(mApplication.getString(R.string.http_response_error), + response.code(), response.message()), response); } if (Headers.class.equals(type)) { @@ -71,13 +68,29 @@ public Object requestSucceed(LifecycleOwner lifecycle, IRequestApi api, Response ResponseBody body = response.body(); if (body == null) { - return null; + throw new NullBodyException(mApplication.getString(R.string.http_response_null_body)); + } + + if (ResponseBody.class.equals(type)) { + return body; + } + + // 如果是用数组接收,判断一下是不是用 byte[] 类型进行接收的 + if(type instanceof GenericArrayType) { + Type genericComponentType = ((GenericArrayType) type).getGenericComponentType(); + if (byte.class.equals(genericComponentType)) { + return body.bytes(); + } } if (InputStream.class.equals(type)) { return body.byteStream(); } + if (Bitmap.class.equals(type)) { + return BitmapFactory.decodeStream(body.byteStream()); + } + String text; try { text = body.string(); @@ -87,30 +100,12 @@ public Object requestSucceed(LifecycleOwner lifecycle, IRequestApi api, Response } // 打印这个 Json 或者文本 - EasyLog.json(text); + EasyLog.printJson(httpRequest, text); if (String.class.equals(type)) { return text; } - if (JSONObject.class.equals(type)) { - try { - // 如果这是一个 JSONObject 对象 - return new JSONObject(text); - } catch (JSONException e) { - throw new DataException(mApplication.getString(R.string.http_data_explain_error), e); - } - } - - if (JSONArray.class.equals(type)) { - try { - // 如果这是一个 JSONArray 对象 - return new JSONArray(text); - } catch (JSONException e) { - throw new DataException(mApplication.getString(R.string.http_data_explain_error), e); - } - } - final Object result; try { @@ -122,13 +117,21 @@ public Object requestSucceed(LifecycleOwner lifecycle, IRequestApi api, Response if (result instanceof HttpData) { HttpData model = (HttpData) result; + Headers headers = response.headers(); + int headersSize = headers.size(); + Map headersMap = new HashMap(headersSize); + for (int i = 0; i < headersSize; i++) { + headersMap.put(headers.name(i), headers.value(i)); + } + // Github issue 地址:https://github.com/getActivity/EasyHttp/issues/233 + model.setResponseHeaders(headersMap); - if (model.isRequestSucceed()) { + if (model.isRequestSuccess()) { // 代表执行成功 return result; } - if (model.isTokenFailure()) { + if (model.isTokenInvalidation()) { // 代表登录失效,需要重新登录 throw new TokenException(mApplication.getString(R.string.http_token_error)); } @@ -139,65 +142,60 @@ public Object requestSucceed(LifecycleOwner lifecycle, IRequestApi api, Response return result; } + @NonNull @Override - public Exception requestFail(LifecycleOwner lifecycle, IRequestApi api, Exception e) { - // 判断这个异常是不是自己抛的 - if (e instanceof HttpException) { - if (e instanceof TokenException) { + public Throwable requestFail(@NonNull HttpRequest httpRequest, @NonNull Throwable throwable) { + if (throwable instanceof HttpException) { + if (throwable instanceof TokenException) { // 登录信息失效,跳转到登录页 - } - return e; + } + return throwable; } - if (e instanceof SocketTimeoutException) { - return new TimeoutException(mApplication.getString(R.string.http_server_out_time), e); + if (throwable instanceof SocketTimeoutException) { + return new TimeoutException(mApplication.getString(R.string.http_server_out_time), throwable); } - if (e instanceof UnknownHostException) { + if (throwable instanceof UnknownHostException) { NetworkInfo info = ((ConnectivityManager) mApplication.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo(); // 判断网络是否连接 if (info != null && info.isConnected()) { // 有连接就是服务器的问题 - return new ServerException(mApplication.getString(R.string.http_server_error), e); + return new ServerException(mApplication.getString(R.string.http_server_error), throwable); } // 没有连接就是网络异常 - return new NetworkException(mApplication.getString(R.string.http_network_error), e); + return new NetworkException(mApplication.getString(R.string.http_network_error), throwable); } - if (e instanceof IOException) { - //e = new CancelException(context.getString(R.string.http_request_cancel), e); - return new CancelException("", e); + if (throwable instanceof IOException) { + // 出现该异常的两种情况 + // 1. 调用 EasyHttp 取消请求 + // 2. 网络请求被中断 + return new CancelException(mApplication.getString(R.string.http_request_cancel), throwable); } - return new HttpException(e.getMessage(), e); - } - - @Override - public Object readCache(LifecycleOwner lifecycle, IRequestApi api, Type type) { - String cacheKey = GsonFactory.getSingletonGson().toJson(api); - String cacheValue = mMmkv.getString(cacheKey, null); - if (cacheValue == null || "".equals(cacheValue) || "{}".equals(cacheValue)) { - return null; - } - EasyLog.print("---------- cacheKey ----------"); - EasyLog.json(cacheKey); - EasyLog.print("---------- cacheValue ----------"); - EasyLog.json(cacheValue); - return GsonFactory.getSingletonGson().fromJson(cacheValue, type); + return new HttpException(throwable.getMessage(), throwable); } + @NonNull @Override - public boolean writeCache(LifecycleOwner lifecycle, IRequestApi api, Response response, Object result) { - String cacheKey = GsonFactory.getSingletonGson().toJson(api); - String cacheValue = GsonFactory.getSingletonGson().toJson(result); - if (cacheValue == null || "".equals(cacheValue) || "{}".equals(cacheValue)) { - return false; - } - EasyLog.print("---------- cacheKey ----------"); - EasyLog.json(cacheKey); - EasyLog.print("---------- cacheValue ----------"); - EasyLog.json(cacheValue); - return mMmkv.putString(cacheKey, cacheValue).commit(); + public Throwable downloadFail(@NonNull HttpRequest httpRequest, @NonNull Throwable throwable) { + if (throwable instanceof ResponseException) { + ResponseException responseException = ((ResponseException) throwable); + Response response = responseException.getResponse(); + responseException.setMessage(String.format(mApplication.getString(R.string.http_response_error), + response.code(), response.message())); + return responseException; + } else if (throwable instanceof NullBodyException) { + NullBodyException nullBodyException = ((NullBodyException) throwable); + nullBodyException.setMessage(mApplication.getString(R.string.http_response_null_body)); + return nullBodyException; + } else if (throwable instanceof FileMd5Exception) { + FileMd5Exception fileMd5Exception = ((FileMd5Exception) throwable); + fileMd5Exception.setMessage(mApplication.getString(R.string.http_response_md5_error)); + return fileMd5Exception; + } + return requestFail(httpRequest, throwable); } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/easy/demo/http/server/ReleaseServer.java b/app/src/main/java/com/hjq/easy/demo/http/server/ReleaseServer.java index 9052005..bf20b0f 100644 --- a/app/src/main/java/com/hjq/easy/demo/http/server/ReleaseServer.java +++ b/app/src/main/java/com/hjq/easy/demo/http/server/ReleaseServer.java @@ -1,5 +1,7 @@ package com.hjq.easy.demo.http.server; +import androidx.annotation.NonNull; + import com.hjq.http.config.IRequestServer; /** @@ -10,13 +12,9 @@ */ public class ReleaseServer implements IRequestServer { + @NonNull @Override public String getHost() { return "https://www.wanandroid.com/"; } - - @Override - public String getPath() { - return ""; - } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/easy/demo/http/server/TestServer.java b/app/src/main/java/com/hjq/easy/demo/http/server/TestServer.java index 32171eb..458385b 100644 --- a/app/src/main/java/com/hjq/easy/demo/http/server/TestServer.java +++ b/app/src/main/java/com/hjq/easy/demo/http/server/TestServer.java @@ -1,5 +1,7 @@ package com.hjq.easy.demo.http.server; +import androidx.annotation.NonNull; + /** * author : Android 轮子哥 * github : https://github.com/getActivity/EasyHttp @@ -8,6 +10,7 @@ */ public class TestServer extends ReleaseServer { + @NonNull @Override public String getHost() { return "https://www.wanandroid.com/"; diff --git a/app/src/main/java/com/hjq/easy/demo/other/ContentResolverUriStore.java b/app/src/main/java/com/hjq/easy/demo/other/ContentResolverUriStore.java new file mode 100644 index 0000000..5c2a275 --- /dev/null +++ b/app/src/main/java/com/hjq/easy/demo/other/ContentResolverUriStore.java @@ -0,0 +1,68 @@ +package com.hjq.easy.demo.other; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresPermission; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2024年01月20日 + * desc : ContentResolver uri 存储器 + */ +public class ContentResolverUriStore { + + private static final String CACHE_PREFERENCES_NAME = "content_resolver_cache_store_preferences"; + + public static @Nullable Uri insert(Context context, @RequiresPermission.Write @NonNull Uri url, @Nullable ContentValues values) { + ContentResolver contentResolver = context.getContentResolver(); + SharedPreferences sharedPreferences = context.getSharedPreferences(CACHE_PREFERENCES_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + // 使用字符串作为缓存键 + String cacheKey = "ContentResolver insert: " + "Uri = " + url + ", ContentValues = " + convertContentValuesToString(values); + String oldUriString = sharedPreferences.getString(cacheKey, ""); + if (oldUriString != null && !oldUriString.isEmpty()) { + Cursor cursor = null; + try { + Uri oldUri = Uri.parse(oldUriString); + // 查询旧的 uri 是否真实并且可用,如果是的话,再进行复用 + cursor = contentResolver.query(oldUri, null, null, null, null); + if (cursor != null && cursor.getCount() != 0) { + return oldUri; + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (cursor != null) { + try { + cursor.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + + Uri newUri = contentResolver.insert(url, values); + // 存储数据到 SharedPreferences + editor.putString(cacheKey, String.valueOf(newUri)); + editor.apply(); + return newUri; + } + + /** + * 将 ContentValues 转换为字符串 + */ + private static String convertContentValuesToString(ContentValues contentValues) { + if (contentValues == null) { + return ""; + } + return contentValues.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 3ddc539..0bf2485 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -10,9 +10,10 @@ tools:context=".MainActivity"> + android:text="@string/btn_get_request" />