Giter Site home page Giter Site logo

xiaoyaoyou1212 / xsnow Goto Github PK

View Code? Open in Web Editor NEW
1.7K 53.0 390.0 20.92 MB

💮基于RxJava2+Retrofit2精心打造的Android基础框架,包含网络、上传、下载、缓存、事件总线、权限管理、数据库、图片加载,基本都是项目中必用功能,每个模块充分解耦,可自由拓展。

Home Page: http://www.huwei.tech/

License: Apache License 2.0

Java 99.97% Batchfile 0.03%
rxjava2 retrofit2 rxbus rxpermissions cache glide greendao

xsnow's Introduction

XSnowIcon


XSnow

Codacy Badge License API

基于RxJava2+Retrofit2精心打造的Android基础框架,包含网络、上传、下载、缓存、事件总线、权限管理、数据库、图片加载,基本都是项目中必用功能,每个模块充分解耦,可自由拓展。

XSnow,X:未知一切,取其通用之意;Snow:雪,取其纯净之意。该框架通用纯净,只依赖公共核心库。

功能简介

  • 支持GET、POST等请求方式,请求与响应数据自动进行转换处理,无需上层分别定义接口;

  • 支持OKHttp本身的Http缓存,也支持外部自定义的在线离线缓存,可配置缓存策略,共有五种缓存策略,如优先获取缓存策略,具体实现参考http包下的strategy包;

  • 支持异常统一处理,定制了ApiException拦截处理,统一返回异常信息;

  • 支持链式编程,数据通过回调返回,也支持返回Observable,可继续采用RxJava的相关特性;

  • 支持失败重试机制,可配置失败重试次数以及重试时间间隔;

  • 支持根据Tag中途取消请求,也可以取消所有请求;

  • 支持泛型T接收处理响应数据,也可根据服务器返回的统一数据模式定制如包含Code、Data、Message的通用Model ApiResult;由于ApiResult的属性不定,无法做到统一处理,所以单独放到netexpand module中,里面包含与其相关的请求处理,可以根据该module定制属于各自服务器的相关功能;

  • 支持全局配置和单个请求的局部配置,如果局部配置与全局配置冲突,那么局部配置会替换全局配置;

  • 全局配置支持CallAdapter.FactoryConverter.Factoryokhttp3.Call.FactorySSLSocketFactoryHostnameVerifierConnectionPool、主机URL、请求头、请求参数、代理、拦截器、Cookie、OKHttp缓存、连接超时时间、读写超时时间、失败重试次数、失败重试间隔时间的一系列配置;

  • 局部请求配置支持主机URL、请求后缀、请求头、请求参数、拦截器、本地缓存策略、本地缓存时间、本地缓存key、连接超时时间、读写超时时间的一系列配置;

  • 支持单文件和多文件上传,上传类型支持图片、字节流、字节数组等,可回调上传进度;

  • 支持小文件下载,可回调下载进度;

  • 支持内存、磁盘二级缓存以及SharedPreferences缓存,可自由拓展。磁盘缓存支持KEY加密存储,可定制缓存时长;SharedPreferences支持内容安全存储,采用Base64加密解密;

  • 支持事件消息发送,采用Rx响应式编程**建立的RxBus模块,采用注解方式标识事件消耗地,通过遍历查找事件处理方法;

  • 支持动态权限申请管理;

  • 支持GreenDao数据库操作;

  • 支持Glide图片加载。

  • ......

效果展示

操作演示动画

使用简介

1、引用依赖

在 build 文件的 dependencies 添加如下依赖:

compile 'com.vise.xiaoyaoyou:xsnow:x.x.x'

其中的 x.x.x 为版本号,下文有具体介绍。

如果需要使用 Glide 加载图片功能,还需要添加如下依赖:

compile 'com.github.bumptech.glide:glide:3.7.0'

如果需要使用GreenDao数据库功能,还需要添加如下依赖:

compile 'org.greenrobot:greendao:3.2.0'

并根据 GreenDao 数据库的使用规则进行相关配置。

2、初始化

在 Application 中进行全局初始化以及添加全局相关配置,简易初始化使用如下:

ViseHttp.init(this);
ViseHttp.CONFIG()
        //配置请求主机地址
        .baseUrl("服务器地址");

如果想快速接入项目中使用只需配置服务器地址就行,其他配置都将采用库中默认的配置。

如果想修改全局相关配置以及查看具体请求调用介绍,请移步 >>> 详细使用介绍文档。

如果想深入了解项目中用到的相关技术,请移步 >>> 相关技术说明文档。

代码托管

JCenter

相关代码 Release 版本已上传到 JCenter,如需查看相关版本记录可点击上面链接进行查询。

版本说明

LatestVersion

如需查看相关版本记录点击上面进入相关链接查看即可,下面是最新修改记录:

  • V2.1.9(2018-06-08)
    • 修改处理数据转换的线程为IO线程;
    • 增加针对Type为String时不解析功能;
    • 修改权限管理持有上下文引用问题。

混淆配置

由于 XSnow 库有依赖部分第三方库,所以需要对依赖的第三方库也做相应的混淆保护,具体的混淆配置如下:

#glide
-dontwarn com.bumptech.glide.**
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
  **[] $VALUES;
  public *;
}

#gson
-dontwarn com.google.gson.**
-keep class com.google.gson.** { *; }

#rxjava
-dontwarn io.reactivex.**
-keep class io.reactivex.** { *; }

#okhttp
-dontwarn okio.**
-keep class okio.** { *; }
-dontwarn okhttp3.**
-keep class okhttp3.** { *; }

#retrofit
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions

#greendao
-dontwarn org.greenrobot.greendao.**
-keep class org.greenrobot.greendao.** { *; }
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
    public static java.lang.String TABLENAME;
}
-keep class **$Properties

针对 XSnow 库本身的混淆保护配置如下:

#XSnow
-dontwarn com.vise.utils.**
-keep class com.vise.xsnow.event.inner.ThreadMode { *; }
-keep class com.vise.xsnow.http.api.ApiService { *; }
-keep class com.vise.xsnow.http.mode.CacheMode
-keep class com.vise.xsnow.http.mode.CacheResult { *; }
-keep class com.vise.xsnow.http.mode.DownProgress { *; }
-keep class com.vise.xsnow.http.strategy.**
-keepclassmembers class * {
    @com.vise.xsnow.event.Subscribe <methods>;
}
-keep class com.bumptech.glide.Glide

如果有拷贝使用到拓展库 netexpand,那么需要保护 ApiResult 这个类,具体配置如下:

#netexpand
-keep class com.vise.netexpand.mode.ApiResult { *; }

注意事项

  • 该框架引用了日志系统和公共工具库,这两个库都很轻量级,具体使用详情可分别参考https://github.com/xiaoyaoyou1212/ViseLoghttps://github.com/xiaoyaoyou1212/ViseUtils

  • 项目中的每个功能都附有使用示例,但示例可能不是很全,主要是提供一个使用的思路,有些现象需要通过查看日志才能看到,如数据库的增删改查操作、事件总线的通知功能等。

  • ==网络访问的API调试采用的是moco服务进行处理的,项目中有提供开启该服务的命令,需要在使用时调用命令开启该服务,还有需要将应用初始化的baseurl设置为本地电脑的IP地址。(重点注意)==

  • 上传文件功能由于使用的是公司内部服务器调试,所以在demo中只提供了一个使用示例,无法看到效果。

关于我

一位热爱分享的技术爱好者!

人生就是一场游戏,你遇到的障碍就是各种关卡,所以,让我们尽情地去攻略吧!

WebsiteGitHubCSDN

最后


如果觉得该项目有帮助,请点下 Star,也欢迎推荐给你的朋友。


如果感觉自己从项目中学到了很多,请不要吝惜你的钱包给点打赏鼓励吧,一分钱也是爱呀! 微信支付


如果有好的想法和建议,也欢迎Fork项目参与进来,使用中如果有任何问题和建议都可以进群交流,QQ群二维码如下:

QQ群 (此群已满)

QQ群


xsnow's People

Contributors

xiaoyaoyou1212 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

xsnow's Issues

请问如果未登录,怎么跳转去登录.

public static ApiException handleException(Throwable e) {
ApiException ex;
if (e instanceof HttpException) {
HttpException httpException = (HttpException) e;
ex = new ApiException(e, ApiCode.Request.HTTP_ERROR);
switch (httpException.code()) {
case ApiCode.Http.UNAUTHORIZED:
case ApiCode.Http.FORBIDDEN:
case ApiCode.Http.NOT_FOUND:
case ApiCode.Http.REQUEST_TIMEOUT:
case ApiCode.Http.GATEWAY_TIMEOUT:
case ApiCode.Http.INTERNAL_SERVER_ERROR:
case ApiCode.Http.BAD_GATEWAY:
case ApiCode.Http.SERVICE_UNAVAILABLE:
default:
ex.message = "NETWORK_ERROR";
break;
}
return ex;
}

这段代码真的不是在作死吗?? 限制了对网络错误的处理. 作为框架应该是去开放功能,而不是去乱限制前端的UI处理吧. 真实人才啊.

CookiesStore.java 可能会有问题

if (cookie.persistent()) {//需要持久化
//将cookies持久化到本地
prefsWriter.putString(url.host(), TextUtils.join(",", cookies.get(url.host()).keySet()));
prefsWriter.putString(name, encodeCookie(new OkHttpCookies(cookie)));
prefsWriter.apply();
} else {//不需要持久化
prefsWriter.remove(url.host());
prefsWriter.remove(name);
prefsWriter.apply();
}

这段代码,会导致所需的cookie 丢失

多个 .baseUrl 的问题

现在公司采用了,您的网络框架。请问博主,如果我想配置多个 baseUrl 怎么处理呢?现在大部分接口,都是一个baseUrl,我直接在application中配置就可以了。但是有的接口,需要另一个baseUrl,如此一来,让我不知怎么办了。麻烦博主,指教一下吧,谢谢!

关于 BaseActivity 的问题

我看到 BaseActivity 里面有个 mViews 用来缓存 View ,我感觉没什么用啊,在用的时候肯定是 find 完了存下来,你这样再存到 BaseActivity 里面缓存一遍,使用频度比较低吧,还是说有什么别的用途

拦截器有关问题

请问一下楼主,在哪里可以拦截到请求的地址,参数等信息呢,HttpLogInterceptor这个里面只能看到地址的请求方式呢。

下载速度太快导致下载失败

这是一个尴尬的问题哈~

当下载速度大概大于1500KB/s时,会导致下载失败,原因:
rx.exceptions.MissingBackpressureException
当下载速度过快时,所发射数据快于处理数据的速度,就会出现这个问题。

retrofit自定义请求问题

ViseHttp.RETROFIT()
.create(AuthorService.class)
.getAuthor()
.compose(ApiTransformer.norTransformer())
.subscribe(new ApiCallbackSubscriber<>(new ACallback() {
@OverRide
public void onSuccess(AuthorModel authorModel) {
ViseLog.i("request onSuccess!");
if (authorModel == null) {
return;
}
mShow_response_data.setText(authorModel.toString());
}

        @Override
        public void onFail(int errCode, String errMsg) {
            ViseLog.e("request errorCode:" + errCode + ",errorMsg:" + errMsg);
        }
    }));

retrofit自定义请求通过拦截器查看是成功的,但是回调走了onFail 方法,这是什么原因呢?

2.1.9版本 解决自定义ApiException 响应处理返回code的方法

应用场景: 当服务器返回 类似 { “code”:400, "message:"用户不存在"} 这样的返回数据,调用demo的 ApiPostRequest, 返回的exception 为: code :1000, msg: UNKNOWN。 这明显与我们预期的返回:“用户不存在” 提示有冲突。
解决方法: 参考了该#2 issues 的解决思路

代码如下:
1、修改ApiBaseRequest

`protected ObservableTransformer<ApiResult, T> apiTransformer() {

    return new ObservableTransformer<ApiResult<T>, T>() {
        @Override
        public ObservableSource<T> apply(Observable<ApiResult<T>> apiResultObservable) {
            return apiResultObservable
                    .subscribeOn(Schedulers.io())
                    .unsubscribeOn(Schedulers.io())
                    .flatMap(new Function<ApiResult<T>, Observable<T>>() {
                        @Override
                        public Observable<T> apply(ApiResult<T> tApiResult) throws Exception {
                            if (ResponseHelper.isSuccess(tApiResult)) {
                                return Observable.just(tApiResult.getData());
                            } else {
                                ApiException e = new ApiException(new Throwable(tApiResult.getMsg()), tApiResult.getCode());
                                return Observable.error(e);
                            }
                        }
                    })
                    .observeOn(AndroidSchedulers.mainThread())
                    .retryWhen(new ApiRetryFunc<T>(retryCount, retryDelayMillis));
        }
    };
}`

2、在ApiException文件,新增内容:
else if (e instanceof ApiException){
ex = new ApiException(e, ((ApiException) e).getCode());
ex.message = e.getLocalizedMessage();
return ex;
}

至此,便可成功响应服务器返回码。
附上我的最终处理结果。
image
最后,由衷感谢博主提供的开源框架。

异常信息

ViseHttp.POST("login")
.baseUrl("http://121.41.44.177/")
.addParam("mobile", "152502016")
.addParam("password", "wrewefa")
.request(new ACallback() {
@OverRide
public void onFail(int errCode, String errMsg) {
ViseLog.e("request errorCode:" + errCode + ",errorMsg:" + errMsg);
}

                @Override
                public void onSuccess(String data) {
                    ViseLog.i("request onSuccess!" + data);
                    mShow_response_data.setText(data);
                }
            });

应用场景: 当服务器返回 类似 { “code”:400, "message:"用户不存在"} 这样的返回数据,调用demo的 ViseHttp.POST, 返回的exception 为: code :1000, msg: UNKNOWN。 这明显与我们预期的返回:“用户不存在” 提示有冲突。请问大佬这应该怎么去改呢?

使用缓存的时候报错;Application和具体调用的配置如下;

日志:io.reactivex.exceptions.ProtocolViolationException: It is not allowed to subscribe with a(n) httplibrary.http.subscriber.ApiCallbackSubscriber multiple times. Please create a fresh instance of .httplibrary.http.subscriber.ApiCallbackSubscriber and subscribe that to the target source instead.
.setHttpCache(true) //配置OkHttp缓存路径 .setHttpCacheDirectory(new File(WeiShuHttp.getContext().getCacheDir(), HttpConfig.CACHE_HTTP_DIR))

Http.BASE(new ApiGetRequest(HttpUrl.Algorithm.algorithmSetting + "/" + BaseApplication.getUserId())) .addHeader("token", BaseApplication.getToken()) .setLocalCache(true) .cacheMode(CacheMode.FIRST_CACHE) .request(new ACallback<RpParamModel>() { @Override public void onSuccess(RpParamModel data) {

ApiResult的list泛型转换问题

有以下Json字符串:

{
  "code": 404,
  "data": 
    {
      "age": 27,
      "name": "qinicy"
    },
  "msg": "success"
}

User的Model:

public class User implements Serializable{
    @SerializedName("name")
    public String name;
    @SerializedName("age")
    public int age;
}

以下是正常的:

 mViseApi.apiGet("http://192.168.199.171:3000/user/", new HashMap<String, String>(), new ApiCallback<User>() {

            @Override
            public void onStart() {
                LogUtil.d("onStart");

            }

            @Override
            public void onError(ApiException e) {

                LogUtil.d("onError");
            }

            @Override
            public void onCompleted() {
                LogUtil.d("onCompleted");
            }

            @Override
            public void onNext(User user) {
                LogUtil.d("onNext");
            }
        });

如果Json变成这样:

{
  "code": 0,
  "data": [
    {
      "age": 27,
      "name": "qinicy"
    },
    {
      "age": 20,
      "name": "Bing"
    }
  ],
  "msg": "success"
}

如何正确获取得到List?
以下不可行:

mViseApi.apiGet("http://192.168.199.171:3000/user/", new HashMap<String, String>(), new ApiCallback<List<User>>() {

           @Override
           public void onStart() {
               LogUtil.d("onStart");

           }

           @Override
           public void onError(ApiException e) {

               LogUtil.d("onError");
           }

           @Override
           public void onCompleted() {
               LogUtil.d("onCompleted");
           }

           @Override
           public void onNext(List<User> userList) {
               LogUtil.d("onNext");
           }
       });

这是由于内部逻辑中:
ClassUtil.getTClass(callback))
得到的Class并不是期望的List,而是User,从容导致最后Gson转换失败。

这个问题如何解决呢?

rxBus注解问题

如demo中所示:同一个AuthorEvent对象,但是post发送参数也都是AuthorEvent,如果我有两个接受不同的事件(携带的都是AuthorEvent,但是要做的是不一样的,怎么区分);
因此,我想起,post可以携带参数发送,即post("tag",AuthorEvent);但是我发现你这个框架中,接收事件注解类
@retention(RetentionPolicy.RUNTIME)
@target(ElementType.METHOD)
public @interface Subscribe {
ThreadMode threadMode() default ThreadMode.MAIN_THREAD;
}
有问题,
@subscribe 后面没法添加(“tag”),编译报错,请问如何修改 @interface Subscribe 这个接受事件注解类呢?

4G 网络请求失败

所用的请求,只有在wifi下才能请求成功,4G网络下请求失败~

是否应该把Retrofit从Request中抽离出来更好?

大概地看了2.0版本的网络模块,相对于1.0版本,2.0版本灵活度更高功能也更完善了。但在看到BaseRequest#generateLocalConfig这里,总感觉怪怪的:这意味着每一次网络请求,都会新建一个Retrofit对象,但其实每一个Retrofit的config其实是一样的(baseUrl == null时)。所以我觉得每一次新建一个Retrofit是非必要的。

一点小小建议

建议使用mvp模式或者mvvm模式 然后在Application里面定义统一异常处理 现在控件一般几乎不会去findViewById直接用butterknife 网络这一块可以用开源的api来实现一个demo即可 然后重点讲一下加密请求就可以啦 上传和下载一般小型app用得不多 根据自己需求来加就ok了 暂时就这些

类型转换问题

ApiResultFunc中String类型需要单独处理,我看了ApiFunc中有处理,为什么ApiResultFunc中不加入处理呢,实测会引起类型转换异常,"期望String,实际是一个Object"

登录时设置的全局请求头token 当应用处于后台长时间挂起,回到前台时token会失效

@xiaoyaoyou1212 登录时设置的全局token 当应用处于后台长时间挂起,回到前台时token会失效,实际上webapi后台token的存活时间是一天,也就是说长时间处于后台全局请求头会被系统回收,导致请求没有token,把token保存在sp中做判断重新设置token也没用, 把请求头定义成静态变量也没用! 后台时间长了一样会被系统回收

关于xpand扩展中的问题

为何ResponseHelper这个返回帮助列中,isSuccess方法中,ignoreSomeIssue部分也是为true,这不该是返回token方面异常失效情况嘛,为何也是true? 因为HttpResponseInterceptor响应拦截需要?那这样的话,其他错误不是在ApiDataFunc( ApiResult转T)全部返回null了啊? 这一串如何理解呢,谢谢

通过添加头信息addhead token验证,无效不起作用

`ViseHttp.GET("api/Report/Get7DayFpyRate")
.addParam("sid", "5")
.addParam("lineId", "431")
.addParam("acctId", "1")
.addHeader("Authorization", "Bearer "+token.trim())
.addHeader("Content-Type","application/json;charset=utf-8")
.request(new ACallback() {
@OverRide
public void onSuccess(Get7DayFpyRateResponse data) {

        }

        @Override
        public void onFail(int errCode, String errMsg) {

            ToastUtils.showShort("errCode: "+errMsg+" errMsg:   "+ errMsg);
        }
    });`

ApiDataFunc中处理服务器响应码的问题

代码如下:

public class ApiDataFunc<T> implements Func1<ApiResult<T>, T> {
    public ApiDataFunc() {
    }

    @Override
    public T call(ApiResult<T> response) {
        if (ApiException.isSuccess(response)) {
            return response.getData();
        } else {
            return (T) new ApiException(new Throwable(response.getMsg()), response.getCode());
        }
    }
}

在call方法里,如果ApiException.isSuccess(response)为false,直接走else里的逻辑:
return (T) new ApiException(new Throwable(response.getMsg()), response.getCode());
上面这一行代码中,ApiException强转T,肯定会出现异常,这个异常发生后,接着在ApiErrFunc处理:

public class ApiErrFunc<T> implements Func1<Throwable, Observable<T>> {
    @Override
    public Observable<T> call(Throwable throwable) {
        return Observable.error(ApiException.handleException(throwable));
    }
}

最后抛出到Subscriber.onError()。
现在问题是,在onError()收到的并不是我们期望的包含response信息的ApiException,而只是一个类型转换错误的Throwable

HttpResponseInterceptor 这个类怎么用

HttpResponseInterceptor 这个类怎么用。现在我想处理AccessToken错误或已过期、帐号在其它手机已登录等等这些操作,但是这些操作是放在UI层的,怎么设计会好一点

编译报错?

Error:org.gradle.api.internal.tasks.DefaultTaskInputs$TaskInputUnionFileCollection cannot be cast to org.gradle.api.internal.file.collections.DefaultConfigurableFileCollection
Possible causes for this unexpected error include:

In the case of corrupt Gradle processes, you can also try closing the IDE and then killing all Java processes.

局部配置超时时间配置代码调用api有误

package com.vise.xsnow.http.request;
该类中,generateLocalConfig()方法中,配置超时时间api有误,全部设置的都是连接服务器后的读取时间,在局部配置修改超时时间不生效:
if (readTimeOut > 0) {
newBuilder.readTimeout(readTimeOut, TimeUnit.SECONDS);
}

    if (writeTimeOut > 0) {

//应调用newBuilder.writeTimeout(writeTimeOut, TimeUnit.SECONDS);
newBuilder.readTimeout(writeTimeOut, TimeUnit.SECONDS);
}

    if (connectTimeOut > 0) {

//应调用 newBuilder.connectTimeout(connectTimeOut, TimeUnit.SECONDS);
newBuilder.readTimeout(connectTimeOut, TimeUnit.SECONDS);
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.