優(yōu)雅封裝Retrofit+RxJava聯(lián)網(wǎng)的統(tǒng)一管理類(lèi)

Retrofit的簡(jiǎn)單用法在上一篇文章分分鐘使用Retrofit+Rxjava實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求已經(jīng)做過(guò)介紹了,今天就不贅述了京腥。
今天主要分享一下如何結(jié)合Rxjava赦肃,封裝一個(gè)RetrofitManager管理類(lèi),統(tǒng)一管理聯(lián)網(wǎng)操作公浪。

《一》讓我們先來(lái)看看封裝后的用法:
              RetrofitManager.getInstance().getRequestService().getWeather("北京")
                            .compose(RxSchedulers.io_main())
                            .subscribeWith(new DisposableObserver<Object>() {
                                @Override
                                public void onNext(Object result) {                                
                                    Log.e("TAG", "result=" + result.toString());
                                }

                                @Override
                                public void onError(Throwable e) {
                                    Log.e("TAG", "onError=" + e.getMessage());
                                }

                                @Override
                                public void onComplete() {
                                    Log.e("TAG", "onComplete");
                                }
                            });

封裝后的用法大家看到了他宛,鏈?zhǔn)秸{(diào)用,一步到位欠气,非常簡(jiǎn)潔明了厅各。接下來(lái)我就帶著大家一步步封裝一個(gè)RetrofitManager。

《二》封裝Retrofit+Rxjava的管理類(lèi)RetrofitManager

(1)在app的build.gradle下配置Retrofit和Rxjava相關(guān)的依賴(lài)包

    //rxandroid
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
    //rxjava
    implementation 'io.reactivex.rxjava2:rxjava:2.1.10'
    //retrofit
    implementation "com.squareup.retrofit2:retrofit:2.4.0"
    //gsonConverter
    implementation "com.squareup.retrofit2:converter-gson:2.4.0"
    //rxjavaAdapter
    implementation "com.squareup.retrofit2:adapter-rxjava2:2.4.0"
    //retrofit log打印
    implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1'

(小提醒: Android Studio3.0之后的依賴(lài)预柒,由compile變成了implementation队塘。)
(2)①新建RetrofitManager類(lèi),提供單例

public class RetrofitManager {
    /**
     * 獲取單例
     */
    private static RetrofitManager mInstance;
       public static RetrofitManager getInstance() {
        if (mInstance == null) {
            synchronized (RetrofitManager.class) {
                if (mInstance == null) {
                    mInstance = new RetrofitManager();
                }
            }
        }
        return mInstance;
    }
}

②配置OkHttp宜鸯,構(gòu)建Retrofit對(duì)象

  private static final long DEFAULT_TIMEOUT = 60L;
  public Retrofit getRetrofit() {
        if (retrofit == null) {
            synchronized (RetrofitManager.class) {
                if (retrofit == null) {
                    OkHttpClient mClient = new OkHttpClient.Builder()
                            //添加公共查詢參數(shù)
                            //.addInterceptor(new CommonQueryParamsInterceptor())
                            //.addInterceptor(new MutiBaseUrlInterceptor())
                            //添加header
                            .addInterceptor(new HeaderInterceptor())
                            .addInterceptor(new LoggingInterceptor())//添加請(qǐng)求攔截(可以在此處打印請(qǐng)求信息和響應(yīng)信息)
                            .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
                            .writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
                            .readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
                          //添加https證書(shū),如果有srca.cer的證書(shū)憔古,則可以通過(guò)sslSocketFactory()配置
                          //.sslSocketFactory(getSSLSocketFactory(context, "srca.cer"))
                            .build();
                    retrofit = new Retrofit.Builder()
                            .baseUrl(BASE_URL)//基礎(chǔ)URL 建議以 / 結(jié)尾
                            .addConverterFactory(GsonConverterFactory.create())//設(shè)置 Json 轉(zhuǎn)換器
                            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())//RxJava 適配器
                            .client(mClient)
                            .build();
                }
            }
        }
        return retrofit;
    }

 /**
     * 實(shí)現(xiàn)https請(qǐng)求
     */
    private static SSLSocketFactory getSSLSocketFactory(Context context, String name) {


        if (context == null) {
            throw new NullPointerException("context == null");
        }

        //CertificateFactory用來(lái)證書(shū)生成
        CertificateFactory certificateFactory;
        InputStream inputStream = null;
        Certificate certificate;

        try {
            inputStream = context.getResources().getAssets().open(name);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {

            certificateFactory = CertificateFactory.getInstance("X.509");
            certificate = certificateFactory.generateCertificate(inputStream);

            //Create a KeyStore containing our trusted CAs
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null, null);
            keyStore.setCertificateEntry(name, certificate);

            //Create a TrustManager that trusts the CAs in our keyStore
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(keyStore);

            //Create an SSLContext that uses our TrustManager
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
            return sslContext.getSocketFactory();

        } catch (Exception e) {

        }
        return null;
    }

③通過(guò)代理的方式,創(chuàng)建ApiServe接口的實(shí)例淋袖。

  public ApiService getRequestService() {
        return getRetrofit().create(ApiService.class);
    }

ApiService是一個(gè)自己定義的interface,所有的網(wǎng)絡(luò)請(qǐng)求接口的配置鸿市,都在此接口內(nèi)完成。網(wǎng)絡(luò)請(qǐng)求URL的配置可以參考Retrofit請(qǐng)求參數(shù)的配置

interface ApiService {
    //獲取北京的天氣信息
//    "https://www.sojson.com/open/api/weather/json.shtml?city=" + "北京"
    @GET("weather/json.shtml")
    Observable<Object> getWeather(@Query("city")String city);
    //上傳文件
   @POST("upload/")
    Observable<UserAvatarBean> uploadFile(@Body RequestBody body);
}

④Header的配置

    /**
     * 添加請(qǐng)求頭需要攜帶的參數(shù)
     */
    public class HeaderInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            Request requestBuilder = request.newBuilder()
                    .addHeader("Connection", HEADER_CONNECTION)
                    .addHeader("token", "token-value")
                    .method(request.method(), request.body())
                    .build();
            return chain.proceed(requestBuilder);
        }
    }

⑤Retrofit的log日志打印

 /**
     * log打印:參考:http://blog.csdn.net/csdn_lqr/article/details/61420753
     */
    public class LoggingInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            //這個(gè)chain里面包含了request和response适贸,所以你要什么都可以從這里拿
            Request request = chain.request();
            long t1 = System.nanoTime();//請(qǐng)求發(fā)起的時(shí)間
            String method = request.method();
            JSONObject jsonObject = new JSONObject();
            if ("POST".equals(method) || "PUT".equals(method)) {
                if (request.body() instanceof FormBody) {
                    FormBody body = (FormBody) request.body();
                    if (body != null) {
                        for (int i = 0; i < body.size(); i++) {
                            try {
                                jsonObject.put(body.name(i), body.encodedValue(i));
                            } catch (JSONException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                    Log.e("request", String.format("發(fā)送請(qǐng)求 %s on %s  %nRequestParams:%s%nMethod:%s",
                            request.url(), chain.connection(), jsonObject.toString(), request.method()));
                } else {
                    Buffer buffer = new Buffer();
                    RequestBody requestBody = request.body();
                    if (requestBody != null) {
                        request.body().writeTo(buffer);
                        String body = buffer.readUtf8();
                        Log.e("request", String.format("發(fā)送請(qǐng)求 %s on %s  %nRequestParams:%s%nMethod:%s",
                                request.url(), chain.connection(), body, request.method()));
                    }
                }
            } else {
                Log.e("request", String.format("發(fā)送請(qǐng)求 %s on %s%nMethod:%s",
                        request.url(), chain.connection(), request.method()));
            }
            Response response = chain.proceed(request);
            long t2 = System.nanoTime();//收到響應(yīng)的時(shí)間
            ResponseBody responseBody = response.peekBody(1024 * 1024);
            Log.e("request",
                    String.format("Retrofit接收響應(yīng): %s %n返回json:【%s】 %n耗時(shí):%.1fms",
                            response.request().url(),
                            responseBody.string(),
                            (t2 - t1) / 1e6d
                    ));
            return response;
        }

    }

看一下日志打印的效果灸芳,有了日志打印,我們就能輕松的調(diào)試每個(gè)網(wǎng)絡(luò)請(qǐng)求了拜姿。
image.png

⑥設(shè)置離線時(shí)緩存,我們可以添加一個(gè)CacheInterceptor烙样,在沒(méi)網(wǎng)絡(luò)的時(shí)候,取緩存的response 蕊肥。在這里緩存的位置在Android/data/包名/files/okhttpCache...目錄下谒获。

                     OkHttpClient mClient = new OkHttpClient.Builder()
                         添加離線緩存
                     .cache(new Cache(File(context.getExternalFilesDir("okhttpCache"), ""), 14 * 1024 * 100))
                     .addInterceptor(new CacheInterceptor())
                     .addNetworkInterceptor(new CacheInterceptor())//必須要有,否則會(huì)返回504
                     .build();    

     /**
     * 設(shè)置緩存的攔截器
     */
    public class CacheInterceptor implements Interceptor {

        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            if (!NetUtils.isNetworkConnected(MyApplication.getContext())) {
                request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
            }
            Response response = chain.proceed(request);
            if (NetUtils.isNetworkConnected(MyApplication.getContext())) {
                String cacheControl = request.cacheControl().toString();
                Elog.e("Tag", "有網(wǎng)");
                return response.newBuilder().header("Cache-Control", cacheControl)
                        .removeHeader("Pragma").build();
            } else {
                Elog.e("Tag", "無(wú)網(wǎng)");
                return response.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale=" + "60 * 60 * 24 * 7")
                        .removeHeader("Pragma").build();
            }
        }
    }

判斷網(wǎng)絡(luò)狀態(tài)壁却,需要添加權(quán)限:

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>

下圖為關(guān)閉網(wǎng)絡(luò)時(shí)批狱,獲取到的離線的數(shù)據(jù):


image.png

image.png
《三》OkHttp的攔截器Interceptor

無(wú)論是上面添加header,還是處理log日志打印展东,或是設(shè)置緩存赔硫,配置一些公共請(qǐng)求參數(shù)等等,都是通過(guò)添加攔截器addInterceptor()來(lái)實(shí)現(xiàn)的盐肃,所以攔截器有多重要爪膊,就不用我多說(shuō)了啦~

先舉個(gè)簡(jiǎn)單的栗子权悟,了解一下攔截器是個(gè)什么東西?
官方介紹:攔截器是一種能夠監(jiān)控推盛,重寫(xiě)峦阁,重試調(diào)用的強(qiáng)大機(jī)制。攔截發(fā)出的請(qǐng)求和傳入的響應(yīng)的日志.
打個(gè)比方:鏢局押著一箱元寶走過(guò)一個(gè)山間小路耘成,突然從山上下來(lái)一群山賊攔住了鏢局的去路榔昔,將鏢局身上值錢(qián)的東西搜刮干凈后將其放行。其中山賊相當(dāng)于攔截器瘪菌,鏢局相當(dāng)于一個(gè)正在執(zhí)行任務(wù)的網(wǎng)絡(luò)請(qǐng)求撒会,請(qǐng)求中的參數(shù)就是鏢局?jǐn)y帶的元寶。攔截器可以將網(wǎng)絡(luò)請(qǐng)求攜帶的參數(shù)進(jìn)行修改驗(yàn)證控嗜,然后放行茧彤。這里面其實(shí)設(shè)計(jì)了AOP編程的思想(面向切面編程)。

詳細(xì)了解可參考:
OkHttp攔截器
Interceptors 攔截器
手把手帶你深入剖析 Retrofit 2.0 源碼

RetrofitManager的完整代碼及用到的相關(guān)代碼(包括Retrofit文件的上傳):

/**
 * Created by JoJo on 2018/4/24.
 * wechat:18510829974
 * description:
 */
public class RetrofitManager {
    /**
     * 請(qǐng)求接口實(shí)例對(duì)象
     */
    private static RetrofitManager mInstance;
    private static final long DEFAULT_TIMEOUT = 60L;
    private Retrofit retrofit = null;
    //請(qǐng)求頭信息
    private final String HEADER_CONNECTION = "keep-alive";
    private String userAgent = "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.87 Safari/537.36";

    public static RetrofitManager getInstance() {
        if (mInstance == null) {
            synchronized (RetrofitManager.class) {
                if (mInstance == null) {
                    mInstance = new RetrofitManager();
                }
            }
        }
        return mInstance;
    }

    public Retrofit getRetrofit() {
        if (retrofit == null) {
            synchronized (RetrofitManager.class) {
                if (retrofit == null) {
                    OkHttpClient mClient = new OkHttpClient.Builder()
                            //添加公告查詢參數(shù)
//                          .addInterceptor(new CommonQueryParamsInterceptor())
//                          .addInterceptor(new MutiBaseUrlInterceptor())
//                          添加離線緩存
//                          .cache(new Cache(File(context.getExternalFilesDir("okhttpCache"), ""), 14 * 1024 * 100))
//                          .addInterceptor(new CacheInterceptor())
//                          .addNetworkInterceptor(new CacheInterceptor())//必須要有疆栏,否則會(huì)返回504    
                            .addInterceptor(new HeaderInterceptor())
                            .addInterceptor(new LoggingInterceptor())//添加請(qǐng)求攔截(可以在此處打印請(qǐng)求信息和響應(yīng)信息)
                            .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
                            .writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
                            .readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
                            .build();
                    retrofit = new Retrofit.Builder()
                            .baseUrl(API.getInstance().BASE_API_URL)//基礎(chǔ)URL 建議以 / 結(jié)尾
                            .addConverterFactory(GsonConverterFactory.create())//設(shè)置 Json 轉(zhuǎn)換器
                            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())//RxJava 適配器
                            .client(mClient)
                            .build();
                }
            }
        }
        return retrofit;
    }

    public ApiService getRequestService() {
        return getRetrofit().create(ApiService.class);
    }

    /**
     * 設(shè)置公共查詢參數(shù)
     */
    public class CommonQueryParamsInterceptor implements Interceptor {

        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            HttpUrl url = request.url().newBuilder()
                    .addQueryParameter("paramsA", "a")
                    .addQueryParameter("paramsB", "b")
                    .build();
            return chain.proceed(request.newBuilder().url(url).build());
        }
    }
   /**
     * 添加請(qǐng)求頭需要攜帶的參數(shù)
     */
    public class HeaderInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            Request requestBuilder = request.newBuilder()
                    .addHeader("Connection", HEADER_CONNECTION)
                    .addHeader("token", "token-value")
                    //.addHeader("User-Agent",userAgent) //如果天氣的接口報(bào):invalid User-Agent header曾掂,把此處注釋打開(kāi)即可
                    .method(request.method(), request.body())
                    .build();
            return chain.proceed(requestBuilder);
        }
    }

      /**
     * 設(shè)置緩存的攔截器
     */
    public class CacheInterceptor implements Interceptor {

        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            if (!NetUtils.isNetworkConnected(MyApplication.getContext())) {
                request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
            }
            Response response = chain.proceed(request);
            if (NetUtils.isNetworkConnected(MyApplication.getContext())) {
                String cacheControl = request.cacheControl().toString();
                Elog.e("Tag", "有網(wǎng)");
                return response.newBuilder().header("Cache-Control", cacheControl)
                        .removeHeader("Pragma").build();
            } else {
                Elog.e("Tag", "無(wú)網(wǎng)");
                return response.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale=" + "60 * 60 * 24 * 7")
                        .removeHeader("Pragma").build();
            }
        }
    }

    /**
     * log打印:http://blog.csdn.net/csdn_lqr/article/details/61420753
     */
    public class LoggingInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            //這個(gè)chain里面包含了request和response,所以你要什么都可以從這里拿
            Request request = chain.request();
            long t1 = System.nanoTime();//請(qǐng)求發(fā)起的時(shí)間
            String method = request.method();
            JSONObject jsonObject = new JSONObject();
            if ("POST".equals(method) || "PUT".equals(method)) {
                if (request.body() instanceof FormBody) {
                    FormBody body = (FormBody) request.body();
                    if (body != null) {
                        for (int i = 0; i < body.size(); i++) {
                            try {
                                jsonObject.put(body.name(i), body.encodedValue(i));
                            } catch (JSONException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                    Elog.e("request", String.format("發(fā)送請(qǐng)求 %s on %s  %nRequestParams:%s%nMethod:%s",
                            request.url(), chain.connection(), jsonObject.toString(), request.method()));
                } else {
                    Buffer buffer = new Buffer();
                    RequestBody requestBody = request.body();
                    if (requestBody != null) {
                        request.body().writeTo(buffer);
                        String body = buffer.readUtf8();
                        Elog.e("request", String.format("發(fā)送請(qǐng)求 %s on %s  %nRequestParams:%s%nMethod:%s",
                                request.url(), chain.connection(), body, request.method()));
                    }
                }
            } else {
                Elog.e("request", String.format("發(fā)送請(qǐng)求 %s on %s%nMethod:%s",
                        request.url(), chain.connection(), request.method()));
            }
            Response response = chain.proceed(request);
            long t2 = System.nanoTime();//收到響應(yīng)的時(shí)間
            ResponseBody responseBody = response.peekBody(1024 * 1024);
            Elog.e("request",
                    String.format("Retrofit接收響應(yīng): %s %n返回json:【%s】 %n耗時(shí):%.1fms",
                            response.request().url(),
                            responseBody.string(),
                            (t2 - t1) / 1e6d
                    ));
            return response;
        }

    }

    /**
     * 打印log日志:該攔截器用于記錄應(yīng)用中的網(wǎng)絡(luò)請(qǐng)求的信息
     */
    private HttpLoggingInterceptor getHttpLogingInterceptor() {
        HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
            @Override
            public void log(String message) {
                //包含所有的請(qǐng)求信息
                //如果收到響應(yīng)是json才打印
                if ("{".equals(message) || "[".equals(message)) {
                    Log.d("TAG", "收到響應(yīng): " + message);
                }
                Log.d("TAG", "message=" + message);
            }
        });
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        return httpLoggingInterceptor;
    }

    private String BASE_URL_OTHER = "http://wthrcdn.etouch.cn/";

    /**
     * 添加可以處理多個(gè)Baseurl的攔截器:http://blog.csdn.net/qq_36707431/article/details/77680252
     * Retrofit(OKHttp)多BaseUrl情況下url實(shí)時(shí)自動(dòng)替換完美解決方法:https://www.2cto.com/kf/201708/663977.html

//     http://wthrcdn.etouch.cn/weather_mini?city=北京
//    @Headers({"url_name:other"})
//    @GET("weather_mini")
//    Observable<WeatherEntity> getMessage(@Query("city") String city);
     */
    private class MutiBaseUrlInterceptor implements Interceptor {

        @Override
        public Response intercept(Chain chain) throws IOException {
            //獲取request
            Request request = chain.request();
            //從request中獲取原有的HttpUrl實(shí)例oldHttpUrl
            HttpUrl oldHttpUrl = request.url();
            //獲取request的創(chuàng)建者builder
            Request.Builder builder = request.newBuilder();
            //從request中獲取headers壁顶,通過(guò)給定的鍵url_name
            List<String> headerValues = request.headers("url_name");
            if (headerValues != null && headerValues.size() > 0) {
                //如果有這個(gè)header珠洗,先將配置的header刪除,因此header僅用作app和okhttp之間使用
                builder.removeHeader("url_name");
                //匹配獲得新的BaseUrl
                String headerValue = headerValues.get(0);
                HttpUrl newBaseUrl = null;
                if ("other".equals(headerValue)) {
                    newBaseUrl = HttpUrl.parse(BASE_URL_OTHER);
//                } else if ("other".equals(headerValue)) {
//                    newBaseUrl = HttpUrl.parse(BASE_URL_PAY);
                } else {
                    newBaseUrl = oldHttpUrl;
                }
                //在oldHttpUrl的基礎(chǔ)上重建新的HttpUrl若专,修改需要修改的url部分
                HttpUrl newFullUrl = oldHttpUrl
                        .newBuilder()
                        .scheme("http")//更換網(wǎng)絡(luò)協(xié)議,根據(jù)實(shí)際情況更換成https或者h(yuǎn)ttp
                        .host(newBaseUrl.host())//更換主機(jī)名
                        .port(newBaseUrl.port())//更換端口
                        .removePathSegment(0)//移除第一個(gè)參數(shù)v1
                        .build();
                //重建這個(gè)request许蓖,通過(guò)builder.url(newFullUrl).build();
                // 然后返回一個(gè)response至此結(jié)束修改
                Elog.e("Url", "intercept: " + newFullUrl.toString());
                return chain.proceed(builder.url(newFullUrl).build());
            }
            return chain.proceed(request);
        }
    }

    /**
     * Retrofit上傳文件
     *
     * @param mImagePath
     * @return
     */
    public RequestBody getUploadFileRequestBody(String mImagePath) {
        File file = new File(mImagePath);
        //構(gòu)建body
        RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM)
                .addFormDataPart("file", file.getName(), RequestBody.create(MediaType.parse("multipart/form-data"), file))
                .build();
        return requestBody;
    }
}

需要用到的類(lèi):

/**
 * 線程調(diào)度
 */
public class RxSchedulers {
    public static <T> ObservableTransformer<T,T> io_main(){
        return new ObservableTransformer<T, T>() {
            @Override
            public ObservableSource<T> apply(Observable<T> observable) {
                return observable.subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread());
            }
        };
    }
}
《四》最后调衰,附上我的一個(gè)Kotlin編寫(xiě)+組件化開(kāi)發(fā)的開(kāi)源項(xiàng)目Designer

Kotlin+組件化開(kāi)發(fā)實(shí)踐—開(kāi)源項(xiàng)目Designer-App

Designer項(xiàng)目算是傾注了我蠻多心血了膊爪,每個(gè)頁(yè)面和功能都當(dāng)成是上線的App來(lái)做,App的logo還特地做了UI設(shè)計(jì)??力求做到精致和完善嚎莉,其中還包括了很多自己項(xiàng)目開(kāi)發(fā)中的經(jīng)驗(yàn)匯總和對(duì)新技術(shù)的探索和整合米酬,希望對(duì)各位讀者有所幫助,歡迎點(diǎn)個(gè)star趋箩,follow赃额,或者給個(gè)小心心,嘻嘻??也可以分享給你更多的朋友一起學(xué)習(xí)叫确,您的支持是我不斷前進(jìn)的動(dòng)力跳芳。如果有任何問(wèn)題,歡迎在GitHub上給我提issue或者留言竹勉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末飞盆,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌桨啃,老刑警劉巖车胡,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件檬输,死亡現(xiàn)場(chǎng)離奇詭異照瘾,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)丧慈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)析命,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人逃默,你說(shuō)我怎么就攤上這事鹃愤。” “怎么了完域?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵软吐,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我吟税,道長(zhǎng)凹耙,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任肠仪,我火速辦了婚禮肖抱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘异旧。我一直安慰自己意述,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布吮蛹。 她就那樣靜靜地躺著荤崇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪潮针。 梳的紋絲不亂的頭發(fā)上术荤,一...
    開(kāi)封第一講書(shū)人閱讀 52,255評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音然低,去河邊找鬼喜每。 笑死,一個(gè)胖子當(dāng)著我的面吹牛雳攘,可吹牛的內(nèi)容都是我干的带兜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼吨灭,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼刚照!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起喧兄,我...
    開(kāi)封第一講書(shū)人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤无畔,失蹤者是張志新(化名)和其女友劉穎啊楚,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體浑彰,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡恭理,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了郭变。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颜价。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖诉濒,靈堂內(nèi)的尸體忽然破棺而出周伦,到底是詐尸還是另有隱情幕垦,我是刑警寧澤娃胆,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站攒读,受9級(jí)特大地震影響片排,放射性物質(zhì)發(fā)生泄漏寨腔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一划纽、第九天 我趴在偏房一處隱蔽的房頂上張望脆侮。 院中可真熱鬧,春花似錦勇劣、人聲如沸靖避。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)幻捏。三九已至,卻和暖如春命咐,著一層夾襖步出監(jiān)牢的瞬間篡九,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工醋奠, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留榛臼,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓窜司,卻偏偏與公主長(zhǎng)得像沛善,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子塞祈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容