Retrofit2+Rxjava2+Rxandroid+okhttp3+Lifecycle 的MVP网络框架,精简Google官方AAC框架,实现APP生命周期的管理

2023-10-27

一.介绍

目前使用较为广泛的网络请求框架 MVP+Retrofit2+okhttp3+Rxjava2,我于2017年也加入了使用行列,在网上找了许多案例,实际项目开发中解决了一些所谓的坑,总结了些内容与大家共享一下。

1.什么是MVP?

在图中有三个模块view(界面),presenter(控制层),model(数据源)。他们在这个项目中中担任什么角色呢?

2. MVP运行的过程

  • Model: 数据层,负责与网络层和数据库层的逻辑交互。
  • View: UI层,显示数据, 并向Presenter报告用户行为。
  • Presenter: 从Model拿数据,应用到UI层,管理UI的状态,响应用户的行为。
  •  用户在view层告诉presenter我要数据
  • presenter告诉model我要数据
  • model访问网络得到了数据再通知presenter给你我取到的数据
  • presenter 处理好数据 再把数据传递给view
  • 最后view将拿到的数据显示出来给用户观看

3.MVP和MVC的区别

 

     MVC首先就是理解比较容易,技术含量不高,这对开发和维护来说成本较低也易于维护与修改。表现层与业务层分离各司其职,对开发来说很有利,但是MVC的每个构件在使用之前都需要经过彻底的测试,代码难以复用。

     在MVP里,Presenter完全把Model和View进行了分离,主要的程序逻辑在Presenter里实现,而且Presenter与具体的view是没有一点关联的,而是通过定义好的接口进行交互,从而使得在变更View的同时可以保持Presenter不变,可以复用。

       在MVP模式里,View只应该有简单的Set/Get方法,用户输入和设置显示的内容,除此不应该有更多的内容,绝不允许直接访问Model,这就是与MVC最大的不同之处。

二.框架的搭建

1.搭建框架的依赖

采用Retrofit2+Rxjava2+Rxandroid+okhttp3 搭建网络请求框架

implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0'

2.创建工具类:RetrofitUtils、OkHttp3Utils

RetrofitUtils和OkHttp3Utils的特性:

  1. 使用okhttp3作为请求接口;
  2. 以观察者模式创建实例;
  3. 使用gson作为数据转换器;
  4. 添加各种拦截器,如日志拦截,请求头拦截,请求参数拦截等等
  5. 开启数据缓存,无网络时可从缓存读取数据;
  6. 辅助类静态方法获取OkHttp3Utils实例。

详细代码如下:

RetrofitUtils工具类封装

封装可以设置多个BaseUrl,应对项目对接多业务方的需求

public abstract class RetrofitUtils {
    private Retrofit mRetrofit = null;
    private Retrofit mRetrofit2 = null;
    private OkHttpClient mOkHttpClient;

    /**
     * 获取Retrofit对象
     *
     * @return
     */
    public Retrofit getRetrofit() {
        if (null == mRetrofit) {
            if (null == mOkHttpClient) {
                OkHttp3Utils okHttp3Utils = new OkHttp3Utils();
                mOkHttpClient = okHttp3Utils.getOkHttpClient();
            }
            mRetrofit = new Retrofit.Builder()
                    .baseUrl(BaseUrlUtil.BaseServiceUrl)
                    .addConverterFactory(new NullOnEmptyConverterFactory())
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .client(mOkHttpClient)
                    .build();
        }
        return mRetrofit;
    }

    /**
     * 获取Retrofit对象
     *这个主要是为了应对多个BaseUrl而准备的
     * @return
     */
    public Retrofit getRetrofit2() {
        if (null == mRetrofit2) {
            if (null == mOkHttpClient) {
                OkHttp3Utils okHttp3Utils = new OkHttp3Utils();
                mOkHttpClient = okHttp3Utils.getOkHttpClient();
            }
            mRetrofit2 = new Retrofit.Builder()
                    .baseUrl(BaseUrlUtil.BaseServiceUrl2)
                    .addConverterFactory(new NullOnEmptyConverterFactory())
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .client(mOkHttpClient)
                    .build();
        }
        return mRetrofit2;
    }


    public class NullOnEmptyConverterFactory extends Converter.Factory {
        @Override
        public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
            final Converter<ResponseBody, ?> delegate = retrofit.nextResponseBodyConverter(this, type, annotations);
            return new Converter<ResponseBody, Object>() {
                @Override
                public Object convert(ResponseBody body) throws IOException {
                    if (body.contentLength() == 0) return null;
                    return delegate.convert(body);
                }
            };
        }
    }

}

 OkHttp3Utils工具类封装

自定义拦截器,可以按照自己的需求设置请求头的参数,同时对cookies做了自动化管理,对cookers管理更方便

public class OkHttp3Utils {
    private OkHttpClient mOkHttpClient;
    Activity activity = AppManager.topActivity();
    private Handler updateHandler = new Handler() {
        @Override
        public void dispatchMessage(Message msg) {
            super.dispatchMessage(msg);
            if (msg.what == 401) {
                //401 token失效
                if (activity != null && !activity.isDestroyed()) {
                    try {
                        PreferenceHelper.write(PreferenceHelper.DEFAULT_FILE_NAME, AppConfig.PREFER_TOKEN_TAG, "");
                        DialogView dialogView = new DialogView(activity, 180, 180, R.layout.my_dialog, R.style.dialog) {
                            @Override
                            public void isdismiss(int tag) {
                                if (tag == DialogView.CANCEL_BUTTON_CLICK) {

                                }
                            }
                        };
                        dialogView.showdialog2("温馨提示", "登录失效,请重新登录", "去登录", "");
                    } catch (Exception es) {
                        es.printStackTrace();
                    }
                }
            } else if (msg.what == 300) {
                Toast.makeText(activity, "暂无网络", Toast.LENGTH_SHORT).show();
            }
        }
    };

    //设置缓存目录
    private File cacheDirectory = new File(MyApplication.getInstance().getApplicationContext().getCacheDir().getAbsolutePath(), "MyCache");
    private Cache cache = new Cache(cacheDirectory, 10 * 1024 * 1024);

    /**
     * 获取OkHttpClient对象
     *
     * @return
     */
    public OkHttpClient getOkHttpClient() {

        if (null == mOkHttpClient) {

            //同样okhttp3后也使用build设计模式
            mOkHttpClient = new OkHttpClient.Builder()
                    //添加拦截器
                    .addInterceptor(new MyIntercepter())
                    //设置一个自动管理cookies的管理器
                    .cookieJar(new CookiesManager())
                    //添加网络连接器
//                    .addNetworkInterceptor(new CookiesInterceptor(MyApplication.getInstance().getApplicationContext()))
                    //设置请求读写的超时时间
                    .connectTimeout(30, TimeUnit.SECONDS)
                    .writeTimeout(30, TimeUnit.SECONDS)
                    .readTimeout(30, TimeUnit.SECONDS)
                    .cache(cache)//设置缓存
                    .retryOnConnectionFailure(true)//自动重试
                    .build();
        }
        return mOkHttpClient;
    }

    /**
     * 拦截器
     */
    private class MyIntercepter implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();

            if (!isNetworkReachable(MyApplication.instance.getApplicationContext())) {
                updateHandler.sendEmptyMessage(300);
                request = request.newBuilder()
                        .cacheControl(CacheControl.FORCE_CACHE)//无网络时只从缓存中读取
                        .build();
            }
            Request.Builder RequestBuilder = request.newBuilder();
            Request build;

            build = RequestBuilder
                    .removeHeader("User-Agent")
                    .addHeader("User-Agent", getUserAgent())
                    .addHeader("Authorization", "")
                    .build();

            Response response = chain.proceed(build);
            int code = response.code();
            //对个别链接地址做处理
            HttpUrl url = response.request().url();
            System.out.println("我的网址"+url);
            updateHandler.sendEmptyMessage(code);
            if (code == 401) {
                //跳转到登录页面
                updateHandler.sendEmptyMessage(401);
            } else if (code == 402) {
                //跳转到开户审核中界面
                updateHandler.sendEmptyMessage(402);
            } else if (code == 403) {
                //跳转到开户界面
                updateHandler.sendEmptyMessage(403);
            }
            return response;
        }
    }

    private static String getUserAgent() {
        String userAgent = "";
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            try {
                userAgent = WebSettings.getDefaultUserAgent(MyApplication.getInstance().getApplicationContext());
            } catch (Exception e) {
                userAgent = System.getProperty("http.agent");
            }
        } else {
            userAgent = System.getProperty("http.agent");
        }
        StringBuffer sb = new StringBuffer();
        for (int i = 0, length = userAgent.length(); i < length; i++) {
            char c = userAgent.charAt(i);
            if (c <= '\u001f' || c >= '\u007f') {
                sb.append(String.format("\\u%04x", (int) c));
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    /**
     * 自动管理Cookies
     */
    private class CookiesManager implements CookieJar {
        private final PersistentCookieStore cookieStore = new PersistentCookieStore(MyApplication.getInstance().getApplicationContext());

        @Override
        public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
            if (cookies != null && cookies.size() > 0) {
                for (Cookie item : cookies) {
                    cookieStore.add(url, item);
                }
            }
        }

        @Override
        public List<Cookie> loadForRequest(HttpUrl url) {
            List<Cookie> cookies = cookieStore.get(url);
            return cookies;
        }
    }

    /**
     * 判断网络是否可用
     *
     * @param context Context对象
     */
    @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
    public Boolean isNetworkReachable(Context context) {
        ConnectivityManager cm = (ConnectivityManager) context
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo current = cm.getActiveNetworkInfo();
        if (current == null) {
            return false;
        }
        return (current.isAvailable());
    }
}

 线程切换操作的封装

public class BaseNetWork extends RetrofitUtils{

    /**https://github.com/r17171709/Retrofit2Demo
     * 插入观察者
     * @param observable
     * @param observer
     * @param <T>
     */
    public  <T> void setSubscribe(Observable<T> observable, Observer<T> observer) {
        observable.subscribeOn(Schedulers.io())
                .subscribeOn(Schedulers.newThread())//子线程访问网络
                .observeOn(AndroidSchedulers.mainThread())//回调到主线程
                .subscribe(observer);
    }
}

下面就是实体接口的调用

public class UserNetWork extends BaseNetWork {

    protected final NetService service = getRetrofit().create(NetService.class);

    private interface NetService {

        //获取首页轮播图
        @GET("api/AppPubilc/get_lunbotu")
        Observable<LunBoTuEntity> toGetLunBoTuEntity();

    }

    //首页轮播图
    public void toGetLunBoTuEntity(Observer<LunBoTuEntity> observer) {
        setSubscribe(service.toGetLunBoTuEntity(), observer);
    }

}

以上就是Retrofit2+Rxjava2+Rxandroid+okhttp3的高度封装的网络框架,自定义拦截器可以拦截请求地址,动态添加请求头里的参数,同时对网络请求响应code码做相应的操作。面对一个项目对接多个业务方,存在多个BaseUrl,该网络框架封装了可以设置多个BaseUrl的。

那么Retrofit2网络框架的网络框架搭建完了,下面来看一下MVP架构的设计吧,不要走开!

下面是项目的整体架构图

创建BaseView基类,用于添加自定义回调,根据需求可做扩展,此处只封装了些最为常用的方法

public interface BaseView {

    void showLoadingDialog(String msg);

    void dismissLoadingDialog();
    /**
     * 显示错误信息
     *
     * @param msg
     */
    void showError(String msg);
    /**
     * 错误码
     */
    void onErrorCode(BaseModel model);
}

 创建Presenter基类,提供M层和V层通讯桥梁

public interface BasePresenter {
    //默认初始化
    void start();

    //Activity关闭把view对象置为空
    void detach();

    //将网络请求的每一个disposable添加进入CompositeDisposable,再退出时候一并注销
    void addDisposable(Disposable subscription);

    //注销所有请求
    void unDisposable();

}

创建一个PresenterImpl,用于统一处理网络请求的生命周期,在activity退出时统一注销观察者模式,解绑观察者的情况下调用unDisposable()统一解绑,防止Rx造成的内存泄漏。

/**
 * 总控制层
 * @param <V>
 */
public abstract class BasePresenterImpl<V extends BaseView> implements BasePresenter {
    protected V view;
    public BasePresenterImpl(V view) {
        this.view = view;
        start();
    }

    @Override
    public void detach() {
        this.view = null;
        unDisposable();
    }

    @Override
    public void start() {

    }


    //将所有正在处理的Subscription都添加到CompositeSubscription中。统一退出的时候注销观察
    private CompositeDisposable mCompositeDisposable;

    /**
     * 将Disposable添加
     *
     * @param subscription
     */
    @Override
    public void addDisposable(Disposable subscription) {
        //csb 如果解绑了的话添加 sb 需要新的实例否则绑定时无效的
        if (mCompositeDisposable == null || mCompositeDisposable.isDisposed()) {
            mCompositeDisposable = new CompositeDisposable();
        }
        mCompositeDisposable.add(subscription);
    }

    /**
     * 在界面退出等需要解绑观察者的情况下调用此方法统一解绑,防止Rx造成的内存泄漏
     */
    @Override
    public void unDisposable() {
        if (mCompositeDisposable != null) {
            mCompositeDisposable.dispose();
        }
    }

}

创建实体类基类,统一处理后台接口返回的数据,做统一处理

public class TradeSimpleResult implements Serializable{

  /**
   * Success : false
   * StatusCode : 500
   * Message : 处理失败
   * ErrorInfo : {"ErrorMessage":"请输入真实的身份证姓名信息","ErrorCode":"-1"}
   */

  private boolean Success;
  private int StatusCode;
  private String Message;
  private ErrorInfoBean ErrorInfo;

  public boolean isSuccess() {
    return Success;
  }

  public void setSuccess(boolean Success) {
    this.Success = Success;
  }

  public int getStatusCode() {
    return StatusCode;
  }

  public void setStatusCode(int StatusCode) {
    this.StatusCode = StatusCode;
  }

  public String getMessage() {
    return Message;
  }

  public void setMessage(String Message) {
    this.Message = Message;
  }

  public ErrorInfoBean getErrorInfo() {
    return ErrorInfo;
  }

  public void setErrorInfo(ErrorInfoBean ErrorInfo) {
    this.ErrorInfo = ErrorInfo;
  }

  public static class ErrorInfoBean {
    /**
     * ErrorMessage : 请输入真实的身份证姓名信息
     * ErrorCode : -1
     */

    private String ErrorMessage;
    private String ErrorCode;

    public String getErrorMessage() {
      return ErrorMessage;
    }

    public void setErrorMessage(String ErrorMessage) {
      this.ErrorMessage = ErrorMessage;
    }

    public String getErrorCode() {
      return ErrorCode;
    }

    public void setErrorCode(String ErrorCode) {
      this.ErrorCode = ErrorCode;
    }
  }
}

好了,以上就是MVP架构的搭建以及接口返回数据的统一处理。代码比较多,但是前后逻辑是很连贯的。那么下面就来做一个实际的接口请求看看效果。

public class MainActivity extends LifecycleBaseActivity<TestContact.presenter> implements TestContact.view {

    private TextView textView;
    private HashMap<Object, Object> map;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        map = new HashMap<>();
        initView();
        initData();

    }

    private void initData() {
        presenter.getData(map, "first");
    }

    private void initView() {
        textView = (TextView) findViewById(R.id.main_text);
    }

    /**
     * 初始化presenter
     *
     * @return 对应的presenter
     */
    @Override
    public TestContact.presenter initPresenter() {
        return new TestPresenter(this, MainActivity.this);
    }

    /**
     * 设置数据
     * 刷新界面
     *
     * @param lunBoTuEntity 数据源
     */
    @Override
    public void setData(LunBoTuEntity lunBoTuEntity, String tag) {
        if ("LunBoTu".equals(tag)) {
            String imageUrl = lunBoTuEntity.getResult().getList().get(0).getImageUrl();
            System.out.println("图片地址:" + imageUrl);
        }
    }

    @Override
    public void ErrorData(Throwable e) {

    }

    @Override
    public void showLoadingDialog(String msg) {
        textView.setText(msg);
    }

    @Override
    public void dismissLoadingDialog() {

    }

}

以上的LifecycleBaseActivity 和 LifecycleBaseFragment大家可以在下面的链接地址里面去看,这个是Goolge官方架构AAC(Android Architecture Component)的生命周期管理框架,Lifecycle类持有Activity 或 Fragment等组件的生命周期信息,并且允许其他对象观察这些信息。Lifecycle内部使用了两个枚举来跟踪其关联组件的生命周期状态:Event和State。祥见下面分析。可以通过调用Lifecycle类的 addObserver() 方法来添加观察者,如下

  getLifecycle().addObserver(new TestLifeCycle());

我在LifecycleBaseActivity做了部分处理,使用起来更加的便捷、易懂。

现在Retrofit2+Rxjava2+Rxandroid+okhttp3+Lifecycle 的MVP网络框架,结合了Google官方AAC框架,实现APP生命周期的管理整体架构就做好了,文章里涉及带网络框架、MVP架构和生命周期的管理,那么一套完整的App框架就搭建好了。

2019.05.09功能新增:

1.新增token过期自动刷新token,刷新后再请求一次接口的功能

github地址: https://github.com/zengweitao/Treasure

简书地址:https://www.jianshu.com/p/ac0eeadb6151

源码下载地址

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Retrofit2+Rxjava2+Rxandroid+okhttp3+Lifecycle 的MVP网络框架,精简Google官方AAC框架,实现APP生命周期的管理 的相关文章

  • 如何在 Android 中保存相机的临时照片?

    在尝试从相机拍照并将其保存到应用程序的缓存文件夹中时 我没有得到任何可见的结果 应用程序不会崩溃 但在 LogCat 上 当我尝试将 ImageView src 字段设置为刚刚获取的文件的 URI 时 我收到此消息 09 17 14 03
  • Android Studio 3.0 Canary 9 - 无法解析包

    我在 Android Studio 3 0 Canary 9 中遇到几个错误 这些错误是 无法解析 android 软件包 下面列出了一些错误 我刚刚安装了 SDK 的所有额外软件包 但仍然收到 gradle 构建错误 Error 82 1
  • 使用workmanager时Firestore脱机持久性错误

    我正在使用一个WorkManger定期从我的中检索信息Firestore当应用程序处于后台和前台时的数据库 此信息用于根据状态更新 UI 因此不同的状态会添加或删除 UI 的不同部分 第一次运行时效果很好 但是 一旦应用程序处于后台并且Wo
  • 在包“android”中找不到属性“backgroundTint”的资源标识符

    我发现了一些视图 xml 属性 例如backgroundTint backgroundTintMode 但是当我使用它作为视图属性定义时 Eclipse 显示错误 No resource identifier found for attri
  • 卸载后 Web 应用程序不显示“添加到主屏幕”

    这是我第一次创建网络应用程序 我设法解决了这个问题 所以我得到了实际的 chrome 提示 将其添加到主屏幕 然后我从手机上卸载了该网络应用程序 因为我想将其展示给我的同事 但是 屏幕上不再出现提示 问题 这是有意为之的行为还是我的应用程序
  • Android 后退按钮无法与 Flutter 选项卡内的导航器配合使用

    我需要在每个选项卡内有一个导航器 因此当我推送新的小部件时 选项卡栏会保留在屏幕上 代码运行得很好 但是 android 后退按钮正在关闭应用程序而不是运行 Navigator pop import package flutter mate
  • 如何以编程方式检查 AndroidManifest.xml 中是否声明了服务?

    我正在编写一个库 该库提供了一项服务 其他开发人员可以通过将其包含在他们的项目中来使用该服务 因此 我无法控制 AndroidManifest xml 我在文档中解释了要做什么 但一个常见的问题是人们忽略了将适当的 标记添加到其清单中 或者
  • Adobe 是否为其 PDF 阅读器提供 Android SDK 或 API? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我希望能够在我们的应用程序内的视图中显示本地 PDF 文件 在 Android 4 03 下的平板电脑上运行 目前 我们将 Adob eR
  • Android SIP 来电使用带有广播接收器的服务

    大家好 其实我正在尝试创建一个应用程序 支持基于 SIP 通过互联网进行音频呼叫 这里使用本机 sip 我遇到了来电问题 我已经完成了服务的注册部分 但是在接听电话时我无法接听电话 请帮助我 Service file package exa
  • 在 HTTPResponse Android 中跟踪重定向

    我需要遵循 HTTPost 给我的重定向 当我发出 HTTP post 并尝试读取响应时 我得到重定向页面 html 我怎样才能解决这个问题 代码 public void parseDoc final HttpParams params n
  • Android:捕获的图像未显示在图库中(媒体扫描仪意图不起作用)

    我遇到以下问题 我正在开发一个应用程序 用户可以在其中拍照 附加到帖子中 并将图片保存到外部存储中 我希望这张照片也显示在图片库中 并且我正在使用媒体扫描仪意图 但它似乎不起作用 我在编写代码时遵循官方的Android开发人员指南 所以我不
  • 无法展开 RemoteViews - 错误通知

    最近 我收到越来越多的用户收到 RemoteServiceException 错误的报告 我每次给出的堆栈跟踪如下 android app RemoteServiceException Bad notification posted fro
  • 原色(有时)变得透明

    我正在使用最新的 SDK 版本 API 21 和支持库 21 0 2 进行开发 并且在尝试实施新的材料设计指南时遇到了麻烦 材料设计说我需要有我的primary color and my accent color并将它们应用到我的应用程序上
  • 如何在PreferenceActivity中添加工具栏

    我已经使用首选项创建了应用程序设置 但我注意到 我的 PreferenceActivity 中没有工具栏 如何将工具栏添加到我的 PreferenceActivity 中 My code 我的 pref xml
  • Ubuntu 16.04 - Genymotion:找不到 /dev/hw_random

    I install Genymotion on the Ubuntu 16 04 64Bit I created a virtual emulator for Android 6 0 then I run this emulator but
  • Android Studio - Windows 7 上的 Android SDK 问题

    我对 Google i o 2013 上发布的最新开发工具 Android Studio 有疑问 我已经成功安装了该程序并且能够正常启动 我可以导入现有项目并对其进行编辑 但是 当我尝试单击 SDK 管理器图标或 AVD 管理器图标时 或者
  • Android:膨胀布局时出现 StackOverFlowError 和 InvokingTargetException

    首先 对不起我的英语 我在膨胀布局时有一个问题 我有一个自定义视图 从 LinearLayout 扩展而来 称为按钮帮助 我在名为的布局上使用该视图加载活动 我的以下代码在所有设备和模拟器上都能完美运行 但具有 QVGA 屏幕 例如 Sam
  • 将 Intent 包装在 LabeledIntent 中以用于显示目的

    要求 我的应用程序中有一个 共享 按钮 我需要通过 Facebook 分享 我需要选择是否安装原生 Facebook 应用程序 我们的决定是 如果未安装该应用程序 则将用户发送到 facebook com 进行分享 当前状态 我可以检测何时
  • 捕获的图像分辨率太大

    我在做什么 我允许用户捕获图像 将其存储到 SD 卡中并上传到服务器 但捕获图像的分辨率为宽度 4608 像素和高度 2592 像素 现在我想要什么 如何在不影响质量的情况下获得小分辨率图像 例如我可以获取或设置捕获的图像分辨率为原始图像分
  • 如何将 google+ 登录集成到我的 Android 应用程序中?

    大家好 实际上我需要通过我的应用程序从 google 登录人们 现在我阅读了 google 上的文档 其中指出 要允许用户登录 请将 Google Sign In 集成到您的应用中 初始化 GoogleApiClient 对象时 请求 PL

随机推荐