Android MVP架构从入门到精通-真枪实弹


0.jpeg

一. 前言

你是否遇到过Activity/Fragment中成百上千行代码,完全无法维护,看着头疼?

你是否遇到过因后台接口还未写而你不能先写代码逻辑的情况?

你是否遇到过用MVC架构写的项目进行单元测试时的深深无奈?

如果你现在还是用MVC架构模式在写项目,请先转到MVP模式!

二. MVC架构

MVC架构模式最初生根于服务器端的Web开发,后来渐渐能够胜任客户端Web开发,再后来因Android项目由XML和Activity/Fragment组成,慢慢的Android开发者开始使用类似MVC的架构模式开发应用.

0_(1).jpeg

 
M层:模型层(model),主要是实体类,数据库,网络等存在的层面,model将新的数据发送到view层,用户得到数据响应.

V层:视图层(view),一般指XML为代表的视图界面.显示来源于model层的数据.用户的点击操作等事件从view层传递到controller层.

C层:控制层(controller),一般以Activity/Fragment为代表.C层主要是连接V层和M层的,C层收到V层发送过来的事件请求,从M层获取数据,展示给V层.

从上图可以看出M层和V层有连接关系,而Activity有时候既充当了控制层又充当了视图层,导致项目维护比较麻烦.
 
1. MVC架构优缺点
A. 缺点

M层和V层有连接关系,没有解耦,导致维护困难.

Activity/Fragment中的代码过多,难以维护.

Activity中有很多关于视图UI的显示代码,因此View视图和Activity控制器并不是完全分离的,当Activity类业务过多的时候,会变得难以管理和维护.尤其是当UI的状态数据,跟持久化的数据混杂在一起,变得极为混乱.

B. 优点

控制层和View层都在Activity中进行操作,数据操作方便.

模块职责划分明确.主要划分层M,V,C三个模块.

三. MVP架构

0_(2).jpeg

 
MVP,即是Model,View,Presenter架构模式.看起来类似MVC,其实不然.从上图能看到Model层和View层没有相连接,完全解耦.

用户触碰界面触发事件,View层把事件通知Presenter层,Presenter层通知Model层处理这个事件,Model层处理后把结果发送到Presenter层,Presenter层再通知View层,最后View层做出改变.这是一整套流程.

M层:模型层(Model),此层和MVC中的M层作用类似.

V层:视图层(View),在MVC中V层只包含XML文件,而MVP中V层包含XML,Activity和Fragment三者.理论上V层不涉及任何逻辑,只负责界面的改变,尽量把逻辑处理放到M层.

P层:通知层(Presenter),P层的主要作用就是连接V层和M层,起到一个通知传递数据的作用.

1. MVP架构优缺点
A. 缺点

MVP中接口过多.

每一个功能,相比于MVC要多写好几个文件.

如果某一个界面中需要请求多个服务器接口,这个界面文件中会实现很多的回调接口,导致代码繁杂.

如果更改了数据源和请求中参数,会导致更多的代码修改.

额外的代码复杂度及学习成本.

B. 优点

模块职责划分明显,层次清晰,接口功能清晰.

Model层和View层分离,解耦.修改View而不影响Model.

功能复用度高,方便.一个Presenter可以复用于多个View,而不用更改Presenter的逻辑.

有利于测试驱动开发,以前的Android开发是难以进行单元测试.

如果后台接口还未写好,但已知返回数据类型的情况下,完全可以写出此接口完整的功能.

四. MVP架构实战(真枪实弹)
1. MVP三层代码简单书写

接下来笔者从简到繁,一点一点的堆砌MVP的整个架构.先看一下XML布局,布局中一个Button按钮和一个TextView控件,用户点击按钮后,Presenter层通知Model层请求处理网络数据,处理后Model层把结果数据发送给Presenter层,Presenter层再通知View层,然后View层改变TextView显示的内容.
 

0.gif

 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
tools:context=".view.SingleInterfaceActivity">

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100px"
android:text="请点击上方按钮获取数据" />
</LinearLayout>

接下来是Activity代码,里面就是获取Button和TextView控件,然后对Button做监听,先简单的这样写,一会慢慢的增加代码.
 
public class SingleInterfaceActivity extends AppCompatActivity {

private Button button;
private TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});

}
}

下面是Model层代码.本次网络请求用的是wanandroid网站的开放api,其中的文章首页列表接口.SingleInterfaceModel文件里面有一个方法getData,第一个参数curPage意思是获取第几页的数据,第二个参数callback是Model层通知Presenter层的回调.
 
public class SingleInterfaceModel {

public void getData(int curPage, final Callback callback) {
NetUtils.getRetrofit()
.create(Api.class)
.getData(curPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<ArticleListBean>() {
@Override
public void onCompleted() {
LP.w("completed");
}

@Override
public void onError(Throwable e) {
callback.onFail("出现错误");
}

@Override
public void onNext(ArticleListBean bean) {
if (null == bean) {
callback.onFail("出现错误");
} else if (bean.errorCode != 0) {
callback.onFail(bean.errorMsg);
} else {
callback.onSuccess(bean);
}
}
});
}
}
Callback文件内容如下.里面一个成功一个失败的回调接口,参数全是泛型,为啥使用泛型笔者就不用说了吧.
 
public interface Callback<K, V> {
void onSuccess(K data);

void onFail(V data);
}
再接下来是Presenter层的代码.SingleInterfacePresenter类构造函数中直接new了一个Model层对象,用于Presenter层对Model层的调用.然后SingleInterfacePresenter类的方法getData用于与Model的互相连接.
 
public class SingleInterfacePresenter {
private final SingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码

}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码

}
});
}
}
至此,MVP三层简单的部分代码算是完成.那么怎样进行整个流程的相互调用呢.我们把刚开始的SingleInterfaceActivity代码改一下,让SingleInterfaceActivity持有Presenter层的对象,这样View层就可以调用Presenter层了.修改后代码如下.
 
public class SingleInterfaceActivity extends AppCompatActivity {

private Button button;
private TextView textView;
private SingleInterfacePresenter singleInterfacePresenter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

singleInterfacePresenter = new SingleInterfacePresenter();
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
singleInterfacePresenter.getData(0);
}
});

}
}
从以上所有代码可以看出,当用户点击按钮后,View层按钮的监听事件执行调用了Presenter层对象的getData方法,此时,Presenter层对象的getData方法调用了Model层对象的getData方法,Model层对象的getData方法中执行了网络请求和逻辑处理,把成功或失败的结果通过Callback接口回调给了Presenter层,然后Presenter层再通知View层改变界面.但此时SingleInterfacePresenter类中收到Model层的结果后无法通知View层,因为SingleInterfacePresenter未持有View层的对象.如下代码的注释中有说明.(如果此时点击按钮,下方代码LP.w()处会打印出网络请求成功的log)
 
public class SingleInterfacePresenter {
private final SingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码

}
});
}
}
代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第一次修改"克隆此时的代码.

2. P层V层沟通桥梁

现在P层未持有V层对象,不能通知V层改变界面,那么就继续演变MVP架构.
在MVP架构中,我们要为每个Activity/Fragment写一个接口,这个接口需要让Presenter层持有,P层通过这个接口去通知V层更改界面.接口中包含了成功和失败的回调,这个接口Activity/Fragment要去实现,最终P层才能通知V层.
 
public interface SingleInterfaceIView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}
一个完整的项目以后肯定会有许多功能界面,那么我们应该抽出一个IView公共接口,让所有的Activity/Fragment都间接实现它.IVew公共接口是用于给View层的接口继承的,注意,不是View本身继承.因为它定义的是接口的规范, 而其他接口才是定义的类的规范(这句话请仔细理解).
public interface IView {
}
这个接口中可以写一些所有Activigy/Fragment共用的方法,我们把SingleInterfaceIView继承IView接口.
public interface SingleInterfaceIView extends IView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}
同理Model层和Presenter层也是如此.
public interface IModel {
}
public interface IPresenter {
}
现在项目中Model层是一个SingleInterfaceModel类,这个类对象被P层持有,对于面向对象设计来讲,利用接口达到解耦目的已经人尽皆知,那我们就要对SingleInterfaceModel类再写一个可继承的接口.代码如下.
public interface ISingleInterfaceModel extends IModel {
void getData(int curPage, final Callback callback);
}
如此,SingleInterfaceModel类的修改如下.
 
public class SingleInterfaceModel implements ISingleInterfaceModel {

@Override
public void getData(int curPage, final Callback callback) {
NetUtils.getRetrofit()
.create(Api.class)
.getData(curPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<ArticleListBean>() {
@Override
public void onCompleted() {
LP.w("completed");
}

@Override
public void onError(Throwable e) {
callback.onFail("出现错误");
}

@Override
public void onNext(ArticleListBean bean) {
if (null == bean) {
callback.onFail("出现错误");
} else if (bean.errorCode != 0) {
callback.onFail(bean.errorMsg);
} else {
callback.onSuccess(bean);
}
}
});
}
}
同理,View层持有P层对象,我们也需要对P层进行改造.但是下面的代码却没有像ISingleInterfaceModel接口继承IModel一样继承IPresenter,这点需要注意,笔者把IPresenter的继承放在了其他处,后面会讲解.
 
public interface ISingleInterfacePresenter {
void getData(int curPage);
}

然后SingleInterfacePresenter类的修改如下:
 
public class SingleInterfacePresenter implements ISingleInterfacePresenter {
private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
}
});
}
}
3. 生命周期适配

至此,MVP三层每层的接口都写好了.但是P层连接V层的桥梁还没有搭建好,这个慢慢来,一个好的高楼大厦都是一步一步建造的.上面IPresenter接口我们没有让其他类继承,接下来就讲下这个.P层和V层相连接,V层的生命周期也要适配到P层,P层的每个功能都要适配生命周期,这里可以把生命周期的适配放在IPresenter接口中.P层持有V层对象,这里把它放到泛型中.代码如下.
 
public interface IPresenter<T extends IView> {

    /**
     * 依附生命view
     *
     * @param view
     */
    void attachView(T view);

    /**
     * 分离View
     */
    void detachView();

    /**
     * 判断View是否已经销毁
     *
     * @return
     */
    boolean isViewAttached();

}
 
这个IPresenter接口需要所有的P层实现类继承,对于生命周期这部分功能都是通用的,那么就可以抽出来一个抽象基类BasePresenter,去实现IPresenter的接口.
public abstract class BasePresenter<T extends IView> implements IPresenter<T> {
protected T mView;

@Override
public void attachView(T view) {
mView = view;
}

@Override
public void detachView() {
mView = null;
}

@Override
public boolean isViewAttached() {
return mView != null;
}
}
此时,SingleInterfacePresenter类的代码修改如下.泛型中的SingleInterfaceIView可以理解成对应的Activity,P层此时完成了对V层的通信.
public class SingleInterfacePresenter extends BasePresenter<SingleInterfaceIView> implements ISingleInterfacePresenter {
private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
if (isViewAttached()) {
mView.showArticleSuccess(loginResultBean);
}
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
if (isViewAttached()) {
mView.showArticleFail(errorMsg);
}
}
});
}
}
此时,P层和V层的连接桥梁已经搭建,但还未搭建完成,我们需要写个BaseMVPActvity让所有的Activity继承,统一处理Activity相同逻辑.在BaseMVPActvity中使用IPresenter的泛型,因为每个Activity中需要持有P层对象,这里把P层对象抽出来也放在BaseMVPActvity中.同时BaseMVPActvity中也需要继承IView,用于P层对V层的生命周期中.代码如下.
 
public abstract class BaseMVPActivity<T extends IPresenter> extends AppCompatActivity implements IView {

protected T mPresenter;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initPresenter();
init();
}

protected void initPresenter() {
mPresenter = createPresenter();
//绑定生命周期
if (mPresenter != null) {
mPresenter.attachView(this);
}
}

@Override
protected void onDestroy() {
if (mPresenter != null) {
mPresenter.detachView();
}
super.onDestroy();
}

/**
* 创建一个Presenter
*
* @return
*/
protected abstract T createPresenter();

protected abstract void init();

}
接下来让SingleInterfaceActivity实现这个BaseMVPActivity.
public class SingleInterfaceActivity extends BaseMVPActivity<SingleInterfacePresenter> implements SingleInterfaceIView {

private Button button;
private TextView textView;

@Override
protected void init() {
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPresenter.getData(0);
}
});
}

@Override
protected SingleInterfacePresenter createPresenter() {
return new SingleInterfacePresenter();
}


@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}
到此,MVP架构的整个简易流程完成.

代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第二次修改"克隆此时的代码.

4. 优化MVP架构

0_(3).jpeg

 
 
上面是MVP的目录,从目录中我们可以看到一个功能点(网络请求)MVP三层各有两个文件需要写,相对于MVC来说写起来确实麻烦,这也是一些人不愿意写MVP,宁愿用MVC的原因.

这里我们可以对此优化一下.MVP架构中有个Contract的概念,Contract有统一管理接口的作用,目的是为了统一管理一个页面的View和Presenter接口,用Contract可以减少部分文件的创建,比如P层和V层的接口文件.

那我们就把P层的ISingleInterfacePresenter接口和V层的SingleInterfaceIView接口文件删除掉,放入SingleInterfaceContract文件中.代码如下.
 
public interface SingleInterfaceContract {


interface View extends IView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}

interface Presenter {
void getData(int curPage);
}


}
此时,SingleInterfacePresenter和SingleInterfaceActivity的代码修改如下.
 
public class SingleInterfacePresenter extends BasePresenter<SingleInterfaceContract.View>
implements SingleInterfaceContract.Presenter {

private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
if (isViewAttached()) {
mView.showArticleSuccess(loginResultBean);
}
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
if (isViewAttached()) {
mView.showArticleFail(errorMsg);
}
}
});
}
}

public class SingleInterfaceActivity extends BaseMVPActivity<SingleInterfacePresenter>
implements SingleInterfaceContract.View {

private Button button;
private TextView textView;

@Override
protected void init() {
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPresenter.getData(0);
}
});
}

@Override
protected SingleInterfacePresenter createPresenter() {
return new SingleInterfacePresenter();
}


@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}

代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第三次修改"克隆此时的代码.

5. 单页面多网络请求以及P层复用

上面的MVP封装只适用于单页面一个网络请求的情况,当一个界面有两个网络请求时,此封装已不适合.以及考虑到P层的复用.为此,我们再次新建一个MultipleInterfaceActivity来进行说明.XML中布局是两个按钮两个Textview,点击则可以进行网络请求.

0_(1).gif

 
 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
tools:context=".view.MultipleInterfaceActivity">

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50px"
android:text="请点击上方按钮获取数据" />

<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100px"
android:text="点击" />

<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50px"
android:text="请点击上方按钮获取数据" />
</LinearLayout>
MultipleInterfaceActivity类代码暂时如下.
 
public class MultipleInterfaceActivity extends BaseMVPActivity {

private Button button;
private TextView textView;
private Button btn;
private TextView tv;


@Override
protected void init() {
setContentView(R.layout.activity_multiple_interface);

button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});


btn = findViewById(R.id.btn);
tv = findViewById(R.id.tv);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});
}

@Override
protected IPresenter createPresenter() {
return null;
}

}
此时我们可以想下,当一个页面中有多个网络请求时,Activity所继承的BaseMVPActivity的泛型中要写多个参数,那有没有上面代码的框架不变的情况下实现这个需求呢?答案必须有的.我们可以把多个网络请求的功能当做一个网络请求来看待,封装成一个MultiplePresenter,其继承至BasePresenter实现生命周期的适配.此MultiplePresenter类的作用就是容纳多个Presenter,连接同一个View.代码如下.
 
public class MultiplePresenter<T extends IView> extends BasePresenter<T> {
private T mView;

private List<IPresenter> presenters = new ArrayList<>();

@SafeVarargs
public final <K extends IPresenter<T>> void addPresenter(K... addPresenter) {
for (K ap : addPresenter) {
ap.attachView(mView);
presenters.add(ap);
}
}

public MultiplePresenter(T mView) {
this.mView = mView;
}

@Override
public void detachView() {
for (IPresenter presenter : presenters) {
presenter.detachView();
}
}

}
因MultiplePresenter类中需要有多个网络请求,现在举例说明时,暂时用两个网络请求接口.MultipleInterfaceActivity类中代码改造如下.
 
public class MultipleInterfaceActivity extends BaseMVPActivity<MultiplePresenter>
implements SingleInterfaceContract.View, MultipleInterfaceContract.View {

private Button button;
private TextView textView;
private Button btn;
private TextView tv;
private SingleInterfacePresenter singleInterfacePresenter;
private MultipleInterfacePresenter multipleInterfacePresenter;


@Override
protected void init() {
setContentView(R.layout.activity_multiple_interface);

button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
singleInterfacePresenter.getData(0);
}
});


btn = findViewById(R.id.btn);
tv = findViewById(R.id.tv);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
multipleInterfacePresenter.getBanner();
}
});
}

@Override
protected MultiplePresenter createPresenter() {
MultiplePresenter multiplePresenter = new MultiplePresenter(this);

singleInterfacePresenter = new SingleInterfacePresenter();
multipleInterfacePresenter = new MultipleInterfacePresenter();

multiplePresenter.addPresenter(singleInterfacePresenter);
multiplePresenter.addPresenter(multipleInterfacePresenter);
return multiplePresenter;
}

@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}

@Override
public void showMultipleSuccess(BannerBean bean) {
tv.setText(bean.data.get(0).title);
}

@Override
public void showMultipleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}
写到这里,MVP框架基本算是完成.如果想再次优化,其实还是有可优化的地方,比如当View销毁时,现在只是让P层中的View对象置为null,并没有继续对M层通知.如果View销毁时,M层还在请求网络中呢,可以为此再加入一个取消网络请求的通用功能.这里只是举一个例子,每个人对MVP的理解不一样,而MVP架构也并不是一成不变,适合自己项目的才是最好的.

6. 完整项目地址

完整项目已提交到github(https://github.com/serge66/MVPDemo).点击下方阅读原文即可访问.

五. 参考资料

[一步步带你精通MVP](https://mp.weixin.qq.com/s/DuNbl3V4gZY-ZCETbhZGug)

[从0到1搭建MVP框架](https://mp.weixin.qq.com/s/QFpHhC-5JkAb4IlMP0nKug)

[Presenter层如何高度的复用](https://juejin.im/post/599ce8016fb9a0247e4255f4)
 
六. 后续

MVVM架构从入门到精通-真枪实弹 敬请期待~~~
 

qrcode_for_gh_08bfa7313fb2_258.jpg

微信公众号:IT大前端
关注可了解更多的大前端领域技术
 
继续阅读 »

0.jpeg

一. 前言

你是否遇到过Activity/Fragment中成百上千行代码,完全无法维护,看着头疼?

你是否遇到过因后台接口还未写而你不能先写代码逻辑的情况?

你是否遇到过用MVC架构写的项目进行单元测试时的深深无奈?

如果你现在还是用MVC架构模式在写项目,请先转到MVP模式!

二. MVC架构

MVC架构模式最初生根于服务器端的Web开发,后来渐渐能够胜任客户端Web开发,再后来因Android项目由XML和Activity/Fragment组成,慢慢的Android开发者开始使用类似MVC的架构模式开发应用.

0_(1).jpeg

 
M层:模型层(model),主要是实体类,数据库,网络等存在的层面,model将新的数据发送到view层,用户得到数据响应.

V层:视图层(view),一般指XML为代表的视图界面.显示来源于model层的数据.用户的点击操作等事件从view层传递到controller层.

C层:控制层(controller),一般以Activity/Fragment为代表.C层主要是连接V层和M层的,C层收到V层发送过来的事件请求,从M层获取数据,展示给V层.

从上图可以看出M层和V层有连接关系,而Activity有时候既充当了控制层又充当了视图层,导致项目维护比较麻烦.
 
1. MVC架构优缺点
A. 缺点

M层和V层有连接关系,没有解耦,导致维护困难.

Activity/Fragment中的代码过多,难以维护.

Activity中有很多关于视图UI的显示代码,因此View视图和Activity控制器并不是完全分离的,当Activity类业务过多的时候,会变得难以管理和维护.尤其是当UI的状态数据,跟持久化的数据混杂在一起,变得极为混乱.

B. 优点

控制层和View层都在Activity中进行操作,数据操作方便.

模块职责划分明确.主要划分层M,V,C三个模块.

三. MVP架构

0_(2).jpeg

 
MVP,即是Model,View,Presenter架构模式.看起来类似MVC,其实不然.从上图能看到Model层和View层没有相连接,完全解耦.

用户触碰界面触发事件,View层把事件通知Presenter层,Presenter层通知Model层处理这个事件,Model层处理后把结果发送到Presenter层,Presenter层再通知View层,最后View层做出改变.这是一整套流程.

M层:模型层(Model),此层和MVC中的M层作用类似.

V层:视图层(View),在MVC中V层只包含XML文件,而MVP中V层包含XML,Activity和Fragment三者.理论上V层不涉及任何逻辑,只负责界面的改变,尽量把逻辑处理放到M层.

P层:通知层(Presenter),P层的主要作用就是连接V层和M层,起到一个通知传递数据的作用.

1. MVP架构优缺点
A. 缺点

MVP中接口过多.

每一个功能,相比于MVC要多写好几个文件.

如果某一个界面中需要请求多个服务器接口,这个界面文件中会实现很多的回调接口,导致代码繁杂.

如果更改了数据源和请求中参数,会导致更多的代码修改.

额外的代码复杂度及学习成本.

B. 优点

模块职责划分明显,层次清晰,接口功能清晰.

Model层和View层分离,解耦.修改View而不影响Model.

功能复用度高,方便.一个Presenter可以复用于多个View,而不用更改Presenter的逻辑.

有利于测试驱动开发,以前的Android开发是难以进行单元测试.

如果后台接口还未写好,但已知返回数据类型的情况下,完全可以写出此接口完整的功能.

四. MVP架构实战(真枪实弹)
1. MVP三层代码简单书写

接下来笔者从简到繁,一点一点的堆砌MVP的整个架构.先看一下XML布局,布局中一个Button按钮和一个TextView控件,用户点击按钮后,Presenter层通知Model层请求处理网络数据,处理后Model层把结果数据发送给Presenter层,Presenter层再通知View层,然后View层改变TextView显示的内容.
 

0.gif

 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
tools:context=".view.SingleInterfaceActivity">

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100px"
android:text="请点击上方按钮获取数据" />
</LinearLayout>

接下来是Activity代码,里面就是获取Button和TextView控件,然后对Button做监听,先简单的这样写,一会慢慢的增加代码.
 
public class SingleInterfaceActivity extends AppCompatActivity {

private Button button;
private TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});

}
}

下面是Model层代码.本次网络请求用的是wanandroid网站的开放api,其中的文章首页列表接口.SingleInterfaceModel文件里面有一个方法getData,第一个参数curPage意思是获取第几页的数据,第二个参数callback是Model层通知Presenter层的回调.
 
public class SingleInterfaceModel {

public void getData(int curPage, final Callback callback) {
NetUtils.getRetrofit()
.create(Api.class)
.getData(curPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<ArticleListBean>() {
@Override
public void onCompleted() {
LP.w("completed");
}

@Override
public void onError(Throwable e) {
callback.onFail("出现错误");
}

@Override
public void onNext(ArticleListBean bean) {
if (null == bean) {
callback.onFail("出现错误");
} else if (bean.errorCode != 0) {
callback.onFail(bean.errorMsg);
} else {
callback.onSuccess(bean);
}
}
});
}
}
Callback文件内容如下.里面一个成功一个失败的回调接口,参数全是泛型,为啥使用泛型笔者就不用说了吧.
 
public interface Callback<K, V> {
void onSuccess(K data);

void onFail(V data);
}
再接下来是Presenter层的代码.SingleInterfacePresenter类构造函数中直接new了一个Model层对象,用于Presenter层对Model层的调用.然后SingleInterfacePresenter类的方法getData用于与Model的互相连接.
 
public class SingleInterfacePresenter {
private final SingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码

}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码

}
});
}
}
至此,MVP三层简单的部分代码算是完成.那么怎样进行整个流程的相互调用呢.我们把刚开始的SingleInterfaceActivity代码改一下,让SingleInterfaceActivity持有Presenter层的对象,这样View层就可以调用Presenter层了.修改后代码如下.
 
public class SingleInterfaceActivity extends AppCompatActivity {

private Button button;
private TextView textView;
private SingleInterfacePresenter singleInterfacePresenter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

singleInterfacePresenter = new SingleInterfacePresenter();
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
singleInterfacePresenter.getData(0);
}
});

}
}
从以上所有代码可以看出,当用户点击按钮后,View层按钮的监听事件执行调用了Presenter层对象的getData方法,此时,Presenter层对象的getData方法调用了Model层对象的getData方法,Model层对象的getData方法中执行了网络请求和逻辑处理,把成功或失败的结果通过Callback接口回调给了Presenter层,然后Presenter层再通知View层改变界面.但此时SingleInterfacePresenter类中收到Model层的结果后无法通知View层,因为SingleInterfacePresenter未持有View层的对象.如下代码的注释中有说明.(如果此时点击按钮,下方代码LP.w()处会打印出网络请求成功的log)
 
public class SingleInterfacePresenter {
private final SingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码

}
});
}
}
代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第一次修改"克隆此时的代码.

2. P层V层沟通桥梁

现在P层未持有V层对象,不能通知V层改变界面,那么就继续演变MVP架构.
在MVP架构中,我们要为每个Activity/Fragment写一个接口,这个接口需要让Presenter层持有,P层通过这个接口去通知V层更改界面.接口中包含了成功和失败的回调,这个接口Activity/Fragment要去实现,最终P层才能通知V层.
 
public interface SingleInterfaceIView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}
一个完整的项目以后肯定会有许多功能界面,那么我们应该抽出一个IView公共接口,让所有的Activity/Fragment都间接实现它.IVew公共接口是用于给View层的接口继承的,注意,不是View本身继承.因为它定义的是接口的规范, 而其他接口才是定义的类的规范(这句话请仔细理解).
public interface IView {
}
这个接口中可以写一些所有Activigy/Fragment共用的方法,我们把SingleInterfaceIView继承IView接口.
public interface SingleInterfaceIView extends IView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}
同理Model层和Presenter层也是如此.
public interface IModel {
}
public interface IPresenter {
}
现在项目中Model层是一个SingleInterfaceModel类,这个类对象被P层持有,对于面向对象设计来讲,利用接口达到解耦目的已经人尽皆知,那我们就要对SingleInterfaceModel类再写一个可继承的接口.代码如下.
public interface ISingleInterfaceModel extends IModel {
void getData(int curPage, final Callback callback);
}
如此,SingleInterfaceModel类的修改如下.
 
public class SingleInterfaceModel implements ISingleInterfaceModel {

@Override
public void getData(int curPage, final Callback callback) {
NetUtils.getRetrofit()
.create(Api.class)
.getData(curPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<ArticleListBean>() {
@Override
public void onCompleted() {
LP.w("completed");
}

@Override
public void onError(Throwable e) {
callback.onFail("出现错误");
}

@Override
public void onNext(ArticleListBean bean) {
if (null == bean) {
callback.onFail("出现错误");
} else if (bean.errorCode != 0) {
callback.onFail(bean.errorMsg);
} else {
callback.onSuccess(bean);
}
}
});
}
}
同理,View层持有P层对象,我们也需要对P层进行改造.但是下面的代码却没有像ISingleInterfaceModel接口继承IModel一样继承IPresenter,这点需要注意,笔者把IPresenter的继承放在了其他处,后面会讲解.
 
public interface ISingleInterfacePresenter {
void getData(int curPage);
}

然后SingleInterfacePresenter类的修改如下:
 
public class SingleInterfacePresenter implements ISingleInterfacePresenter {
private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
}
});
}
}
3. 生命周期适配

至此,MVP三层每层的接口都写好了.但是P层连接V层的桥梁还没有搭建好,这个慢慢来,一个好的高楼大厦都是一步一步建造的.上面IPresenter接口我们没有让其他类继承,接下来就讲下这个.P层和V层相连接,V层的生命周期也要适配到P层,P层的每个功能都要适配生命周期,这里可以把生命周期的适配放在IPresenter接口中.P层持有V层对象,这里把它放到泛型中.代码如下.
 
public interface IPresenter<T extends IView> {

    /**
     * 依附生命view
     *
     * @param view
     */
    void attachView(T view);

    /**
     * 分离View
     */
    void detachView();

    /**
     * 判断View是否已经销毁
     *
     * @return
     */
    boolean isViewAttached();

}
 
这个IPresenter接口需要所有的P层实现类继承,对于生命周期这部分功能都是通用的,那么就可以抽出来一个抽象基类BasePresenter,去实现IPresenter的接口.
public abstract class BasePresenter<T extends IView> implements IPresenter<T> {
protected T mView;

@Override
public void attachView(T view) {
mView = view;
}

@Override
public void detachView() {
mView = null;
}

@Override
public boolean isViewAttached() {
return mView != null;
}
}
此时,SingleInterfacePresenter类的代码修改如下.泛型中的SingleInterfaceIView可以理解成对应的Activity,P层此时完成了对V层的通信.
public class SingleInterfacePresenter extends BasePresenter<SingleInterfaceIView> implements ISingleInterfacePresenter {
private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
if (isViewAttached()) {
mView.showArticleSuccess(loginResultBean);
}
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
if (isViewAttached()) {
mView.showArticleFail(errorMsg);
}
}
});
}
}
此时,P层和V层的连接桥梁已经搭建,但还未搭建完成,我们需要写个BaseMVPActvity让所有的Activity继承,统一处理Activity相同逻辑.在BaseMVPActvity中使用IPresenter的泛型,因为每个Activity中需要持有P层对象,这里把P层对象抽出来也放在BaseMVPActvity中.同时BaseMVPActvity中也需要继承IView,用于P层对V层的生命周期中.代码如下.
 
public abstract class BaseMVPActivity<T extends IPresenter> extends AppCompatActivity implements IView {

protected T mPresenter;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initPresenter();
init();
}

protected void initPresenter() {
mPresenter = createPresenter();
//绑定生命周期
if (mPresenter != null) {
mPresenter.attachView(this);
}
}

@Override
protected void onDestroy() {
if (mPresenter != null) {
mPresenter.detachView();
}
super.onDestroy();
}

/**
* 创建一个Presenter
*
* @return
*/
protected abstract T createPresenter();

protected abstract void init();

}
接下来让SingleInterfaceActivity实现这个BaseMVPActivity.
public class SingleInterfaceActivity extends BaseMVPActivity<SingleInterfacePresenter> implements SingleInterfaceIView {

private Button button;
private TextView textView;

@Override
protected void init() {
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPresenter.getData(0);
}
});
}

@Override
protected SingleInterfacePresenter createPresenter() {
return new SingleInterfacePresenter();
}


@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}
到此,MVP架构的整个简易流程完成.

代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第二次修改"克隆此时的代码.

4. 优化MVP架构

0_(3).jpeg

 
 
上面是MVP的目录,从目录中我们可以看到一个功能点(网络请求)MVP三层各有两个文件需要写,相对于MVC来说写起来确实麻烦,这也是一些人不愿意写MVP,宁愿用MVC的原因.

这里我们可以对此优化一下.MVP架构中有个Contract的概念,Contract有统一管理接口的作用,目的是为了统一管理一个页面的View和Presenter接口,用Contract可以减少部分文件的创建,比如P层和V层的接口文件.

那我们就把P层的ISingleInterfacePresenter接口和V层的SingleInterfaceIView接口文件删除掉,放入SingleInterfaceContract文件中.代码如下.
 
public interface SingleInterfaceContract {


interface View extends IView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}

interface Presenter {
void getData(int curPage);
}


}
此时,SingleInterfacePresenter和SingleInterfaceActivity的代码修改如下.
 
public class SingleInterfacePresenter extends BasePresenter<SingleInterfaceContract.View>
implements SingleInterfaceContract.Presenter {

private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
if (isViewAttached()) {
mView.showArticleSuccess(loginResultBean);
}
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
if (isViewAttached()) {
mView.showArticleFail(errorMsg);
}
}
});
}
}

public class SingleInterfaceActivity extends BaseMVPActivity<SingleInterfacePresenter>
implements SingleInterfaceContract.View {

private Button button;
private TextView textView;

@Override
protected void init() {
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPresenter.getData(0);
}
});
}

@Override
protected SingleInterfacePresenter createPresenter() {
return new SingleInterfacePresenter();
}


@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}

代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第三次修改"克隆此时的代码.

5. 单页面多网络请求以及P层复用

上面的MVP封装只适用于单页面一个网络请求的情况,当一个界面有两个网络请求时,此封装已不适合.以及考虑到P层的复用.为此,我们再次新建一个MultipleInterfaceActivity来进行说明.XML中布局是两个按钮两个Textview,点击则可以进行网络请求.

0_(1).gif

 
 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
tools:context=".view.MultipleInterfaceActivity">

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50px"
android:text="请点击上方按钮获取数据" />

<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100px"
android:text="点击" />

<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50px"
android:text="请点击上方按钮获取数据" />
</LinearLayout>
MultipleInterfaceActivity类代码暂时如下.
 
public class MultipleInterfaceActivity extends BaseMVPActivity {

private Button button;
private TextView textView;
private Button btn;
private TextView tv;


@Override
protected void init() {
setContentView(R.layout.activity_multiple_interface);

button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});


btn = findViewById(R.id.btn);
tv = findViewById(R.id.tv);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});
}

@Override
protected IPresenter createPresenter() {
return null;
}

}
此时我们可以想下,当一个页面中有多个网络请求时,Activity所继承的BaseMVPActivity的泛型中要写多个参数,那有没有上面代码的框架不变的情况下实现这个需求呢?答案必须有的.我们可以把多个网络请求的功能当做一个网络请求来看待,封装成一个MultiplePresenter,其继承至BasePresenter实现生命周期的适配.此MultiplePresenter类的作用就是容纳多个Presenter,连接同一个View.代码如下.
 
public class MultiplePresenter<T extends IView> extends BasePresenter<T> {
private T mView;

private List<IPresenter> presenters = new ArrayList<>();

@SafeVarargs
public final <K extends IPresenter<T>> void addPresenter(K... addPresenter) {
for (K ap : addPresenter) {
ap.attachView(mView);
presenters.add(ap);
}
}

public MultiplePresenter(T mView) {
this.mView = mView;
}

@Override
public void detachView() {
for (IPresenter presenter : presenters) {
presenter.detachView();
}
}

}
因MultiplePresenter类中需要有多个网络请求,现在举例说明时,暂时用两个网络请求接口.MultipleInterfaceActivity类中代码改造如下.
 
public class MultipleInterfaceActivity extends BaseMVPActivity<MultiplePresenter>
implements SingleInterfaceContract.View, MultipleInterfaceContract.View {

private Button button;
private TextView textView;
private Button btn;
private TextView tv;
private SingleInterfacePresenter singleInterfacePresenter;
private MultipleInterfacePresenter multipleInterfacePresenter;


@Override
protected void init() {
setContentView(R.layout.activity_multiple_interface);

button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
singleInterfacePresenter.getData(0);
}
});


btn = findViewById(R.id.btn);
tv = findViewById(R.id.tv);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
multipleInterfacePresenter.getBanner();
}
});
}

@Override
protected MultiplePresenter createPresenter() {
MultiplePresenter multiplePresenter = new MultiplePresenter(this);

singleInterfacePresenter = new SingleInterfacePresenter();
multipleInterfacePresenter = new MultipleInterfacePresenter();

multiplePresenter.addPresenter(singleInterfacePresenter);
multiplePresenter.addPresenter(multipleInterfacePresenter);
return multiplePresenter;
}

@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}

@Override
public void showMultipleSuccess(BannerBean bean) {
tv.setText(bean.data.get(0).title);
}

@Override
public void showMultipleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}
写到这里,MVP框架基本算是完成.如果想再次优化,其实还是有可优化的地方,比如当View销毁时,现在只是让P层中的View对象置为null,并没有继续对M层通知.如果View销毁时,M层还在请求网络中呢,可以为此再加入一个取消网络请求的通用功能.这里只是举一个例子,每个人对MVP的理解不一样,而MVP架构也并不是一成不变,适合自己项目的才是最好的.

6. 完整项目地址

完整项目已提交到github(https://github.com/serge66/MVPDemo).点击下方阅读原文即可访问.

五. 参考资料

[一步步带你精通MVP](https://mp.weixin.qq.com/s/DuNbl3V4gZY-ZCETbhZGug)

[从0到1搭建MVP框架](https://mp.weixin.qq.com/s/QFpHhC-5JkAb4IlMP0nKug)

[Presenter层如何高度的复用](https://juejin.im/post/599ce8016fb9a0247e4255f4)
 
六. 后续

MVVM架构从入门到精通-真枪实弹 敬请期待~~~
 

qrcode_for_gh_08bfa7313fb2_258.jpg

微信公众号:IT大前端
关注可了解更多的大前端领域技术
  收起阅读 »

在线直播源码实现直播技术曾遇到的那些小问题

文章主要内容:在直播过程经常会遇到哪些问题?在线直播源码是怎样实现相应的直播技术的?这些问题的产生是由怎样的原因导致的?
以下这些问题,我相信都是直播中十分常见,并且具有一定参考性的问题。大家可以通过以下内容寻找对应的问题和原因,希望能给大家产生一定的帮助。
1.播放失败:服务器连接失败、域名解析失败、只有音频没有视频、只有视频没有音频。
2.直播出现卡顿:(1)主播端网络不好,导致推流上行不稳定。(2)服务端线路质量差,造成分发不稳定。(3)用户端网络质量差,从而拉流下行不稳定。
3.延时高:网络传输延时、协议延时、业务代码中的缓冲区。
4.音画不同步:(应从视频直播的生产端进行排查)采集设备内部出现问题、时间戳没有在采集时被获取、采集源距离太远、时间戳出现回退或紊乱现象、播放端的性能问题。
5.马赛克:图像尺寸原因、视频编码参数配置原因、关键帧丢失。
6.播放黑屏:主播端编码失效、视频编码失效、码流前半段只有音频没有视频。
7.播放花屏:播放器没有从关键帧开始解码、码流中的视频尺寸发生变化、丢失参考帧、硬编解兼容性问题、推流端的图像尺寸格式。
8.播放闪屏:推流端原因、播放器缓冲机制原因。
9.播放杂音(回声):网络波动、回声消除、参数配置、混音越界。
10.拖动不准:直播过程中丢帧、关键帧间隔太大。
11.CPU/GPU占用率高:数据量大、格式转换、软编解格式。
12.在直播过程中,决定视频预加载效果的好坏主要由:视频的码率、缓冲文件大小和网速决定。
原因:网速快且码率低的情况下,不需要使用预加载。(码率中等且网速一般的情况适用)需要注意的是:缓冲文件不能设置过大,会影响正常播放。
12.为什么播放视频时,会停留在第一帧画面。
原因:(1)解码器出现错误,只接出了第一帧图像。(2)没有接收到视频帧。(3)时间戳的计算有误。
   以上内容简单总结了直播中经常出现的问题及原因,那么在文章的结尾,想给大家举个简单的例子,比如盖楼需要混凝土和砖;种树需要土壤和水;养鱼需要水和饲料,开发一个直播平台就需要在线直播源码。源码就是开发的基础,没有源码就无法完成。所以,选择优质的源码也是开发过程中十分重要的一步。
本文声明原创,转载请注明出处。
继续阅读 »
文章主要内容:在直播过程经常会遇到哪些问题?在线直播源码是怎样实现相应的直播技术的?这些问题的产生是由怎样的原因导致的?
以下这些问题,我相信都是直播中十分常见,并且具有一定参考性的问题。大家可以通过以下内容寻找对应的问题和原因,希望能给大家产生一定的帮助。
1.播放失败:服务器连接失败、域名解析失败、只有音频没有视频、只有视频没有音频。
2.直播出现卡顿:(1)主播端网络不好,导致推流上行不稳定。(2)服务端线路质量差,造成分发不稳定。(3)用户端网络质量差,从而拉流下行不稳定。
3.延时高:网络传输延时、协议延时、业务代码中的缓冲区。
4.音画不同步:(应从视频直播的生产端进行排查)采集设备内部出现问题、时间戳没有在采集时被获取、采集源距离太远、时间戳出现回退或紊乱现象、播放端的性能问题。
5.马赛克:图像尺寸原因、视频编码参数配置原因、关键帧丢失。
6.播放黑屏:主播端编码失效、视频编码失效、码流前半段只有音频没有视频。
7.播放花屏:播放器没有从关键帧开始解码、码流中的视频尺寸发生变化、丢失参考帧、硬编解兼容性问题、推流端的图像尺寸格式。
8.播放闪屏:推流端原因、播放器缓冲机制原因。
9.播放杂音(回声):网络波动、回声消除、参数配置、混音越界。
10.拖动不准:直播过程中丢帧、关键帧间隔太大。
11.CPU/GPU占用率高:数据量大、格式转换、软编解格式。
12.在直播过程中,决定视频预加载效果的好坏主要由:视频的码率、缓冲文件大小和网速决定。
原因:网速快且码率低的情况下,不需要使用预加载。(码率中等且网速一般的情况适用)需要注意的是:缓冲文件不能设置过大,会影响正常播放。
12.为什么播放视频时,会停留在第一帧画面。
原因:(1)解码器出现错误,只接出了第一帧图像。(2)没有接收到视频帧。(3)时间戳的计算有误。
   以上内容简单总结了直播中经常出现的问题及原因,那么在文章的结尾,想给大家举个简单的例子,比如盖楼需要混凝土和砖;种树需要土壤和水;养鱼需要水和饲料,开发一个直播平台就需要在线直播源码。源码就是开发的基础,没有源码就无法完成。所以,选择优质的源码也是开发过程中十分重要的一步。
本文声明原创,转载请注明出处。 收起阅读 »

React Native调用原生Android/iOS代码方案并实现拨号功能

一 前言

由于前几个月公司2.0项目开发技术选型为React Native,技术部相关人员开始学习React Native相关的技术,笔者是一名Android开发者,下文所描述的React Native调用Android/iOS模块中关于iOS的部分如有误的地方,请指出。为了让从Android或iOS学习React Native的同志更加清楚的了解另一移动端,笔者尽可能写的详细点。

二 效果

下面两张图分别为iOS和Android上效果图,其中iOS效果图中点击电话号码会打印log,并不会调起iOS拨号界面,因为iOS模拟器不支持此功能,所以要想看效果只能用真机查看。这里打印log是为了证明React Native成功调起了原生iOS模块功能。

640.gif

 

640_(1).gif

 
 
三 实现方案

关于调用拨号功能以及调用浏览器、短信、邮箱等功能,可有两种实现方案。

一种是按照React Native提供的调用原生的过程方案来调用,这种适合大部分React Native调用原生功能的需求,掌握这种后,基本以后再有调用原生需求即可按照此过程方案解决,此文也会选用这种方案进行描述。

另一种是React Native帮我们封装的Linking模块可以实现这类的需求,这种相比上一种来说相对简单,主要适用于调用原生的电话、短信、邮箱、浏览器等功能。

四 实现原生Android模块

1.在自己新建的Reacat Native项目中android/app/src/main/java/xxx(项目包名)/ 目录下(为了和其他文件分离,笔者又在此目录下新建一个native文件夹),需要新建一个java类文件,例如文件名为CallPhoneModule.java,这个java类一定要继承RN提供的ReactContextBaseJavaModule抽象类,然后实现其构造函数,其中的参数要为ReactApplicationContext reactContext。
 
public class CallPhoneModule extends ReactContextBaseJavaModule {
public CallPhoneModule(ReactApplicationContext reactContext) {
super(reactContext);
}
}

 
2.然后实现NativeModule中定义的getName()方法,返回一个String类型字符串,这个返回结果将要在JavaScript中使用,例如返回“CallPhoneModule”,则可以在JavaScript中通过React.NativeModules.CallPhoneModule调用。
 
注意,如果返回的字符串中有RCT前缀,则会自动移除RCT前缀。例如返回“RCTCallPhoneModule”,则在JavaScript中依然可以通过React.NativeModules.CallPhoneModule调用。CallPhoneModule继承ReactContextBaseJavaModule,ReactContextBaseJavaModule继承BaseJavaModule,BaseJavaModule实现了NativeModule接口,这是CallPhoneModule与NativeModule的关系。 
@Override
public String getName() {
return "CallPhoneModule";
}

 
3.然后在CallPhoneModule类中写一个方法,这个方法提供给JavaScript调用,例如方法名为callPhone,里面传递String类型参数(非必须),特别的这个方法要使用@ReactMethod注解,以及返回类型必须为void。然后在callPhone方法中写入要实现的功能,这里写入了拨号功能的实现。
 
Intent intent = new Intent(Intent.ACTION_DIAL,  Uri.parse("tel:" + phoneString));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
this.reactContext.startActivity(intent);

CallPhoneModule.java文件的全部代码如下:
package com.zhuku02;

import android.content.Intent;
import android.net.Uri;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.lang.String;

public class CallPhoneModule extends ReactContextBaseJavaModule {

public ReactApplicationContext reactContext;

public CallPhoneModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

@ReactMethod
public void callPhone(String phoneString) {
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneString));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
this.reactContext.startActivity(intent);
}

@Override
public String getName() {
return "CallPhoneModule";
}
}

4.新建一个java类文件,例如文件名为CallPhoneReactPackage.java,这个类必须实现ReactPackage接口,然后实现createViewManagers、createNativeModules两个方法,特别的要在createNativeModules方法的返回值中add进刚才新建的CallPhoneModule类。

CallPhoneReactPackage.java全部代码如下:
package com.zhuku02;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.zhuku02.CallPhoneModule;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CallPhoneReactPackage implements ReactPackage {

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}

@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();

modules.add(new CallPhoneModule(reactContext));

return modules;
}
}

5.最后在MainApplication.java文件中的getPackages方法中加上刚才新建的CallPhoneReactPackage类。至此,原生Android模块书写完毕。关于JavaScript调用原生Android模块代码会在文末和调用原生iOS一起写出。

修改后的MainApplication.java文件代码如下:
package com.zhuku02;

import android.app.Application;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;

import com.zhuku02.CallPhoneReactPackage;

import java.util.Arrays;
import java.util.List;


public class MainApplication extends Application implements ReactApplication {

private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}

@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new CallPhoneReactPackage()
);
}
};

@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}

@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}

 
五 实现原生iOS模块

1.在自己新建的Reacat Native项目中ios/xxx(项目包名)/ 目录下,需要新建一个后缀为m和一个后缀为h的文件。为了和其他文件分离以及和Android保持一致,笔者又在此目录下新建一个native文件夹。
 
在Xcode的此文件夹上右键New File,然后在弹出的页面中Cocoa Touch Class选项输入文件名,这样会建立出相同文件名的m和h文件。如果New File时,分别选择Objective-C File和Header File,则这两个文件名要相同。例如文件名称为CallPhoneModuleIos。

2.在CallPhoneModuleIos.h文件中,类要实现RN提供的RCTBridgeModule协议。RCT是ReaCT的缩写,React Native中Object-C相关的命名均以RCT开头。
 
RCTBridgeModule是定义好的protocol,实现该协议的类,会自动注册到Object-C对应的Bridge中。Object-C Bridge上层负责与Object-C通信,下层负责和JavaScript Bridge通信,而JavaScript Bridge负责和JavaScript通信。这样,通过Object-C Bridge和JavaScript Bridge就可以实现JavaScript和Object-C的相互调用。

CallPhoneModuleIos.h文件如下:
#import <UIKit/UIKit.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>

@interface CallPhoneModuleIos : NSObject <RCTBridgeModule>

@end

3.CallPhoneModuleIos.m文件中,类需要包含RCT_EXPORT_MODULE()宏,作用是自动注册一个module。这个宏可以添加一个参数,用来指定在JavaScript调用这个模块的名字,类似于上文中说的getName()方法。如果不添加这个参数,则默认就是这个类的名字。

4.然后需要在此类中写一个方法,提供给RN调用,方法通过RCT_EXPORT_METHOD()宏来实现。
RCT_EXPORT_METHOD(callPhone: (NSString *)phone){
NSLog(@"======%@",phone);
}

CallPhoneModuleIos.m完整代码:
#import "CallPhoneModuleIos.h"
#import <Foundation/Foundation.h>

@implementation CallPhoneModuleIos

RCT_EXPORT_MODULE(CallPhoneModuleIos);

RCT_EXPORT_METHOD(callPhone: (NSString *)phone){

NSLog(@"======%@",phone);
//去掉注释,下面代码就是实现拨号功能代码,但还未真机测试
// NSMutableString * str = [[NSMutableString alloc] initWithFormat:@"telprompt://%@",phone];
// [[UIApplication sharedApplication] openURL:[NSURL URLWithString:str]];

}

// -(dispath_queue_t)methodQueue{
// return dispath_get_main_queue();
// }

@end

至此,则原生iOS代码书写完成,现在即将开始调用。

六 React Native调用Android、iOS原生模块

为了在JavaScript端同时访问Android、iOS原生模块更加方便,笔者把原生模块的调用封装在一个JavaScript文件中,例如文件名为CallPhone.js,这样在需要调用的地方直接调用此JavaScript文件既可,同时在此文件中,处理好Android、iOS、Web(若有)的分别调用。
import {Platform, NativeModules} from 'react-native';

var module = null;
if (Platform.OS == 'ios') {
module = NativeModules.CallPhoneModuleIos;
} else if (Platform.OS == 'android') {
module = NativeModules.CallPhoneModule;
} else if (Platform.OS == 'web') {
//暂未实现web功能
}
export default module;

然后在JavaScript文件中这样调用:
import CallPhone from '../../native/CallPhone';
CallPhone.callPhone('40077731xx');

到这里,整篇文章就结束了,疑问、建议或者指教欢迎讨论。

七 参考资料

native-modules-ios (https://facebook.github.io/rea ... s.html)
native-modules-android (https://facebook.github.io/rea ... d.html) 


qrcode_for_gh_08bfa7313fb2_258.jpg

微信公众号:IT大前端
关注可了解更多的大前端领域技术
继续阅读 »
一 前言

由于前几个月公司2.0项目开发技术选型为React Native,技术部相关人员开始学习React Native相关的技术,笔者是一名Android开发者,下文所描述的React Native调用Android/iOS模块中关于iOS的部分如有误的地方,请指出。为了让从Android或iOS学习React Native的同志更加清楚的了解另一移动端,笔者尽可能写的详细点。

二 效果

下面两张图分别为iOS和Android上效果图,其中iOS效果图中点击电话号码会打印log,并不会调起iOS拨号界面,因为iOS模拟器不支持此功能,所以要想看效果只能用真机查看。这里打印log是为了证明React Native成功调起了原生iOS模块功能。

640.gif

 

640_(1).gif

 
 
三 实现方案

关于调用拨号功能以及调用浏览器、短信、邮箱等功能,可有两种实现方案。

一种是按照React Native提供的调用原生的过程方案来调用,这种适合大部分React Native调用原生功能的需求,掌握这种后,基本以后再有调用原生需求即可按照此过程方案解决,此文也会选用这种方案进行描述。

另一种是React Native帮我们封装的Linking模块可以实现这类的需求,这种相比上一种来说相对简单,主要适用于调用原生的电话、短信、邮箱、浏览器等功能。

四 实现原生Android模块

1.在自己新建的Reacat Native项目中android/app/src/main/java/xxx(项目包名)/ 目录下(为了和其他文件分离,笔者又在此目录下新建一个native文件夹),需要新建一个java类文件,例如文件名为CallPhoneModule.java,这个java类一定要继承RN提供的ReactContextBaseJavaModule抽象类,然后实现其构造函数,其中的参数要为ReactApplicationContext reactContext。
 
public class CallPhoneModule extends ReactContextBaseJavaModule {
public CallPhoneModule(ReactApplicationContext reactContext) {
super(reactContext);
}
}

 
2.然后实现NativeModule中定义的getName()方法,返回一个String类型字符串,这个返回结果将要在JavaScript中使用,例如返回“CallPhoneModule”,则可以在JavaScript中通过React.NativeModules.CallPhoneModule调用。
 
注意,如果返回的字符串中有RCT前缀,则会自动移除RCT前缀。例如返回“RCTCallPhoneModule”,则在JavaScript中依然可以通过React.NativeModules.CallPhoneModule调用。CallPhoneModule继承ReactContextBaseJavaModule,ReactContextBaseJavaModule继承BaseJavaModule,BaseJavaModule实现了NativeModule接口,这是CallPhoneModule与NativeModule的关系。 
@Override
public String getName() {
return "CallPhoneModule";
}

 
3.然后在CallPhoneModule类中写一个方法,这个方法提供给JavaScript调用,例如方法名为callPhone,里面传递String类型参数(非必须),特别的这个方法要使用@ReactMethod注解,以及返回类型必须为void。然后在callPhone方法中写入要实现的功能,这里写入了拨号功能的实现。
 
Intent intent = new Intent(Intent.ACTION_DIAL,  Uri.parse("tel:" + phoneString));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
this.reactContext.startActivity(intent);

CallPhoneModule.java文件的全部代码如下:
package com.zhuku02;

import android.content.Intent;
import android.net.Uri;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.lang.String;

public class CallPhoneModule extends ReactContextBaseJavaModule {

public ReactApplicationContext reactContext;

public CallPhoneModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

@ReactMethod
public void callPhone(String phoneString) {
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneString));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
this.reactContext.startActivity(intent);
}

@Override
public String getName() {
return "CallPhoneModule";
}
}

4.新建一个java类文件,例如文件名为CallPhoneReactPackage.java,这个类必须实现ReactPackage接口,然后实现createViewManagers、createNativeModules两个方法,特别的要在createNativeModules方法的返回值中add进刚才新建的CallPhoneModule类。

CallPhoneReactPackage.java全部代码如下:
package com.zhuku02;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.zhuku02.CallPhoneModule;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CallPhoneReactPackage implements ReactPackage {

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}

@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();

modules.add(new CallPhoneModule(reactContext));

return modules;
}
}

5.最后在MainApplication.java文件中的getPackages方法中加上刚才新建的CallPhoneReactPackage类。至此,原生Android模块书写完毕。关于JavaScript调用原生Android模块代码会在文末和调用原生iOS一起写出。

修改后的MainApplication.java文件代码如下:
package com.zhuku02;

import android.app.Application;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;

import com.zhuku02.CallPhoneReactPackage;

import java.util.Arrays;
import java.util.List;


public class MainApplication extends Application implements ReactApplication {

private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}

@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new CallPhoneReactPackage()
);
}
};

@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}

@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}

 
五 实现原生iOS模块

1.在自己新建的Reacat Native项目中ios/xxx(项目包名)/ 目录下,需要新建一个后缀为m和一个后缀为h的文件。为了和其他文件分离以及和Android保持一致,笔者又在此目录下新建一个native文件夹。
 
在Xcode的此文件夹上右键New File,然后在弹出的页面中Cocoa Touch Class选项输入文件名,这样会建立出相同文件名的m和h文件。如果New File时,分别选择Objective-C File和Header File,则这两个文件名要相同。例如文件名称为CallPhoneModuleIos。

2.在CallPhoneModuleIos.h文件中,类要实现RN提供的RCTBridgeModule协议。RCT是ReaCT的缩写,React Native中Object-C相关的命名均以RCT开头。
 
RCTBridgeModule是定义好的protocol,实现该协议的类,会自动注册到Object-C对应的Bridge中。Object-C Bridge上层负责与Object-C通信,下层负责和JavaScript Bridge通信,而JavaScript Bridge负责和JavaScript通信。这样,通过Object-C Bridge和JavaScript Bridge就可以实现JavaScript和Object-C的相互调用。

CallPhoneModuleIos.h文件如下:
#import <UIKit/UIKit.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>

@interface CallPhoneModuleIos : NSObject <RCTBridgeModule>

@end

3.CallPhoneModuleIos.m文件中,类需要包含RCT_EXPORT_MODULE()宏,作用是自动注册一个module。这个宏可以添加一个参数,用来指定在JavaScript调用这个模块的名字,类似于上文中说的getName()方法。如果不添加这个参数,则默认就是这个类的名字。

4.然后需要在此类中写一个方法,提供给RN调用,方法通过RCT_EXPORT_METHOD()宏来实现。
RCT_EXPORT_METHOD(callPhone: (NSString *)phone){
NSLog(@"======%@",phone);
}

CallPhoneModuleIos.m完整代码:
#import "CallPhoneModuleIos.h"
#import <Foundation/Foundation.h>

@implementation CallPhoneModuleIos

RCT_EXPORT_MODULE(CallPhoneModuleIos);

RCT_EXPORT_METHOD(callPhone: (NSString *)phone){

NSLog(@"======%@",phone);
//去掉注释,下面代码就是实现拨号功能代码,但还未真机测试
// NSMutableString * str = [[NSMutableString alloc] initWithFormat:@"telprompt://%@",phone];
// [[UIApplication sharedApplication] openURL:[NSURL URLWithString:str]];

}

// -(dispath_queue_t)methodQueue{
// return dispath_get_main_queue();
// }

@end

至此,则原生iOS代码书写完成,现在即将开始调用。

六 React Native调用Android、iOS原生模块

为了在JavaScript端同时访问Android、iOS原生模块更加方便,笔者把原生模块的调用封装在一个JavaScript文件中,例如文件名为CallPhone.js,这样在需要调用的地方直接调用此JavaScript文件既可,同时在此文件中,处理好Android、iOS、Web(若有)的分别调用。
import {Platform, NativeModules} from 'react-native';

var module = null;
if (Platform.OS == 'ios') {
module = NativeModules.CallPhoneModuleIos;
} else if (Platform.OS == 'android') {
module = NativeModules.CallPhoneModule;
} else if (Platform.OS == 'web') {
//暂未实现web功能
}
export default module;

然后在JavaScript文件中这样调用:
import CallPhone from '../../native/CallPhone';
CallPhone.callPhone('40077731xx');

到这里,整篇文章就结束了,疑问、建议或者指教欢迎讨论。

七 参考资料

native-modules-ios (https://facebook.github.io/rea ... s.html)
native-modules-android (https://facebook.github.io/rea ... d.html) 


qrcode_for_gh_08bfa7313fb2_258.jpg

微信公众号:IT大前端
关注可了解更多的大前端领域技术 收起阅读 »

EaseUI 3.5.3 DexArchiveMergerException

集成easeui3.5.3报错
集成easeui3.5.3报错

【活动推荐】2019安卓巴士千人开发者大会-NEW●无界

   本次将邀请到BAT等知名互联网公司的16位优秀演讲嘉宾坐镇,内容上除了主会场全天的Android技术干货分享,分会场还将覆盖到大数据、机器学习、跨平台开发以及小程序等方向内容。

三人行两人免单团购超便宜报名速戳https://www.hdb.com/dis/htn3yi6fpo
活动时间:2019年4月20号
活动地点:深圳南山区圣淘沙酒店
 
*入现场群说明:

购票成功后,请添加安卓巴士官方微信号 shzsbgbs ,备注【开发者大会】通过后提供购票成功截图,小编邀请您进入420开发者大会现场群。

备注【拼团】邀请您进入拼团群,和小伙伴一起享受超低团购价。

*付费说明:

本次活动经费用于场租、设备、物料、礼品、嘉宾交通住宿费等。

*退款说明:

由于本活动各项资源需提前采购,一经售出不接受退款,请确认后购买。 
 

41551673499360_article4_58412.jpg


41551673130966_article4_57367.jpg


微信图片_20190305183217.jpg

 
继续阅读 »
   本次将邀请到BAT等知名互联网公司的16位优秀演讲嘉宾坐镇,内容上除了主会场全天的Android技术干货分享,分会场还将覆盖到大数据、机器学习、跨平台开发以及小程序等方向内容。

三人行两人免单团购超便宜报名速戳https://www.hdb.com/dis/htn3yi6fpo
活动时间:2019年4月20号
活动地点:深圳南山区圣淘沙酒店
 
*入现场群说明:

购票成功后,请添加安卓巴士官方微信号 shzsbgbs ,备注【开发者大会】通过后提供购票成功截图,小编邀请您进入420开发者大会现场群。

备注【拼团】邀请您进入拼团群,和小伙伴一起享受超低团购价。

*付费说明:

本次活动经费用于场租、设备、物料、礼品、嘉宾交通住宿费等。

*退款说明:

由于本活动各项资源需提前采购,一经售出不接受退款,请确认后购买。 
 

41551673499360_article4_58412.jpg


41551673130966_article4_57367.jpg


微信图片_20190305183217.jpg

  收起阅读 »

WIFI 考勤打卡 浅析

一、背景

最近产品部提出了在WEB端设置wifi考勤打卡新需求,根据管理员设置的wifi相关信息(主要是WIFI名称和MAC地址),员工用户利用移动端相连接的wifi进行wifi考勤打卡。

二、名词术语解释

下面的理解全是建立在无线路由器的基础上。如有错误请指出。

1、SS

SS(Service set)即服务集,是无线局域网中的一个术语,用以描述802.11无线网络的构成单位(一组互相有联系的无线设备),使用服务集标识符(SSID)作为识别。

可以分为独立基本服务集(IBSS)、基本服务集(BSS)和扩展服务集(ESS)三类。其中IBSS属于对等拓扑模式(又称Ad-Hoc模式、无线随意网络),而BSS和ESS属于基础架构模式。这些拓扑是原始的802.11规范中定义的,其他的如网桥、中继器等则是属于特定厂商的扩展或者WDS的拓扑模式。

2、SSID

SSID(Service Set Identifier)即服务集标识符,是一个或一组基础架构模式无线网络的标识,依照标识方式又可细分为两种:
基本服务集标识符(BSSID),表示的是AP的数据链路层的MAC地址。
扩展服务集标识符(ESSID),一个最长32字节区分大小写的字符串,ESSID标识与SSID相同的网络。术语SSID最常用。
在此可以理解为无线路由器发射的某个wifi的名称。(SSID=name of network)

3、BSS

BSS(Basic Service Set)即基本服务集,是一组能在PHY层相互通信的所有站。每个BSS都有一个称为BSSID的标识(ID),它是服务于BSS的接入点的MAC地址。
用在无线路由器发射出的wifi上可以这样理解:某一个无线路由器发射出的wifi信号所覆盖的范围可视为BSS。

WechatIMG363.jpeg

 
4、BSSID

BSSID(Basic Service Set Identifier)即基本服务集标识符。

在上面的基础上可以这样理解:对某一个BSS基本服务集的唯一标识。例如,某无线路由器发射了一个名称为A的wifi热点,同一区域另一个无线路由器也发出了一个名称为A的wifi热点,当手机连接A热点时,如何辨别连接的是由哪一个路由器发射的wifi呢?

这时候就要用到BSSID了。一般情况下BSSID可以理解为无线路由器的MAC地址,通过查看手机连接wifi的MAC地址即可知道连接的是哪一个路由。(BSSID=AP MAC address)
其实准确来说手机得到的BSSID并不是路由器的基准(出厂)MAC地址。

例如,笔者公司的某款无线路由器B的出厂MAC地址为 XX:XX:XX:XX:XX:F1,当手机连接此wifi查看mac地址时发现是XX:XX:XX:XX:XX:F2,或者是XX:XX:XX:XX:XX:F3。

5、ESS ESSID

ESS(Extended Service Set )即扩展的基本服务集。
ESSID(Extended Service Set Identifier)即扩展的基本服务集标识符。
BSS+BSS+BSS+BSS+…=ESS。ESS为多个BSS的集合。ESS使用指定的ESSID作识别。

通过将多个BSS比邻安置,可以扩展网络的范围,如果这些BSS通过各种分布系统互联(无论是有线的还是无线的),拥有一致的ESSID,并且对于逻辑链路控制层来说可以认为是一个BSS的话,那么这些BSS可以被统一为一个ESS。

在同一个ESS中的不同BSS之间切换的过程称为漫游。一般而言,一个ESS中的BSS都会使用相同的SSID和安全机制以提供接近于无缝漫游的可能。两个BSS之间通常有15%左右的重叠范围来保证漫游时信号不会长时间丢失,并且设置在不同频段来防止相互干扰。

WechatIMG362.jpeg

 
6、MAC

MAC地址采用十六进制数表示,共六个字节(48位)。(XX:XX:XX:XX:XX:XX )其中,前三个字节是由IEEE的注册管理机构RA负责给不同厂家分配的代码(高位24位),也称为“编制上唯一的标识符”(Organizationally Unique Identifier),后三个字节(低位24位)由各厂家自行指派给生产的适配器接口,称为扩展标识符(唯一性)。

三、历程

当产品部提出wifi考勤打卡需求时,普遍认为一个路由器有一个mac地址,手机连接wifi可以根据mac地址等信息进行打卡。当我们用多个手机连接公司名称为A(SSID)的wifi时,发现手机上展示的mac地址并不是一致的,这个就尴尬了,打翻了原有理念。
然后发现我们公司共有五个无线路由器,wifi名称都是A。哦,这时候才感觉到原来以前的知识还是靠谱的,可能是多个手机具体连接的路由器不是同一个。

然后把五个路由器wifi热点名称改为A、B、C、D、E,多个手机连接A热点时,发现手机得到的mac地址是一致的,到这里可以得出的结论是手机连接同一个wifi热点得到的mac地址是一致的。但是…..又尴尬了。

当多款手机连接B热点时,发现又出现了不一致的mac地址,查找原因发现,原来B无线路由器中可以设置2.4G Hz和5G Hz两个不同频段的wifi热点。B路由器中默认是开启2.4G Hz和5G Hz频段的wifi热点,并且wifi名称(SSID)是同一个。经过检查还有个问题是B路由器的出厂mac地址和手机连接得到的mac地址不一致。

例如上面举得例子:笔者公司的某款无线路由器B的出厂MAC地址为 XX:XX:XX:XX:XX:F1,当手机连接此wifi查看mac地址时发现是XX:XX:XX:XX:XX:F2,另一款手机连接时是XX:XX:XX:XX:XX:F3。由此可得出的结论是,路由器有一个基准(出厂)mac地址,然后发射出wifi的mac在基准mac地址上按照一定的算法进行变动,具体的变动算法不清楚,有清楚的请告知我,非常感谢。

另外还有一个问题是,C路由器设备后面所写的出厂说明mac地址是XX:XX:XX:XX:XX:56,但是通过路由器后台看到的出厂mac地址是XX:XX:XX:XX:XX:57,手机连接后得到的mac地址是XX:XX:XX:XX:XX:56。这就尴尬了,是厂家写错了还是根据特定的算法算的?

除了根据wifi设备分析外,我们也对具有wifi考勤打卡功能的软件进行了分析。比如现在比较火爆的由阿里团队研发的钉钉,以及纷享销客APP,在Android端,他们的处理都是获取周围wifi信息(并不是当前手机连接的wifi)进行打卡。在iOS端,他们的处理都是根据当前手机连接的wifi信息进行打卡。据iOS同事说,iOS获取周围wifi信息需要申请此功能,并最低支持版本是iOS 9。另外据可靠消息,分享逍客对mac地址的处理也是通过忽略低4位进行匹配。

四、结论

经过上述分析,手机获取的无线路由器MAC地址的低4位是变化的。那我们实现这个需求时,除了匹配虚拟位置、手机信息、wifi相关等其他信息外,只针对mac地址,我们可以忽略mac地址的低4位来做匹配。

五、参考资料

http://www.juniper.net/documentation/en_US/junos-space-apps12.3/network-director/topics/concept/wireless-ssid-bssid-essid.html 


qrcode_for_gh_08bfa7313fb2_258.jpg

微信公众号:IT大前端
关注可了解更多的大前端领域技术
继续阅读 »
一、背景

最近产品部提出了在WEB端设置wifi考勤打卡新需求,根据管理员设置的wifi相关信息(主要是WIFI名称和MAC地址),员工用户利用移动端相连接的wifi进行wifi考勤打卡。

二、名词术语解释

下面的理解全是建立在无线路由器的基础上。如有错误请指出。

1、SS

SS(Service set)即服务集,是无线局域网中的一个术语,用以描述802.11无线网络的构成单位(一组互相有联系的无线设备),使用服务集标识符(SSID)作为识别。

可以分为独立基本服务集(IBSS)、基本服务集(BSS)和扩展服务集(ESS)三类。其中IBSS属于对等拓扑模式(又称Ad-Hoc模式、无线随意网络),而BSS和ESS属于基础架构模式。这些拓扑是原始的802.11规范中定义的,其他的如网桥、中继器等则是属于特定厂商的扩展或者WDS的拓扑模式。

2、SSID

SSID(Service Set Identifier)即服务集标识符,是一个或一组基础架构模式无线网络的标识,依照标识方式又可细分为两种:
基本服务集标识符(BSSID),表示的是AP的数据链路层的MAC地址。
扩展服务集标识符(ESSID),一个最长32字节区分大小写的字符串,ESSID标识与SSID相同的网络。术语SSID最常用。
在此可以理解为无线路由器发射的某个wifi的名称。(SSID=name of network)

3、BSS

BSS(Basic Service Set)即基本服务集,是一组能在PHY层相互通信的所有站。每个BSS都有一个称为BSSID的标识(ID),它是服务于BSS的接入点的MAC地址。
用在无线路由器发射出的wifi上可以这样理解:某一个无线路由器发射出的wifi信号所覆盖的范围可视为BSS。

WechatIMG363.jpeg

 
4、BSSID

BSSID(Basic Service Set Identifier)即基本服务集标识符。

在上面的基础上可以这样理解:对某一个BSS基本服务集的唯一标识。例如,某无线路由器发射了一个名称为A的wifi热点,同一区域另一个无线路由器也发出了一个名称为A的wifi热点,当手机连接A热点时,如何辨别连接的是由哪一个路由器发射的wifi呢?

这时候就要用到BSSID了。一般情况下BSSID可以理解为无线路由器的MAC地址,通过查看手机连接wifi的MAC地址即可知道连接的是哪一个路由。(BSSID=AP MAC address)
其实准确来说手机得到的BSSID并不是路由器的基准(出厂)MAC地址。

例如,笔者公司的某款无线路由器B的出厂MAC地址为 XX:XX:XX:XX:XX:F1,当手机连接此wifi查看mac地址时发现是XX:XX:XX:XX:XX:F2,或者是XX:XX:XX:XX:XX:F3。

5、ESS ESSID

ESS(Extended Service Set )即扩展的基本服务集。
ESSID(Extended Service Set Identifier)即扩展的基本服务集标识符。
BSS+BSS+BSS+BSS+…=ESS。ESS为多个BSS的集合。ESS使用指定的ESSID作识别。

通过将多个BSS比邻安置,可以扩展网络的范围,如果这些BSS通过各种分布系统互联(无论是有线的还是无线的),拥有一致的ESSID,并且对于逻辑链路控制层来说可以认为是一个BSS的话,那么这些BSS可以被统一为一个ESS。

在同一个ESS中的不同BSS之间切换的过程称为漫游。一般而言,一个ESS中的BSS都会使用相同的SSID和安全机制以提供接近于无缝漫游的可能。两个BSS之间通常有15%左右的重叠范围来保证漫游时信号不会长时间丢失,并且设置在不同频段来防止相互干扰。

WechatIMG362.jpeg

 
6、MAC

MAC地址采用十六进制数表示,共六个字节(48位)。(XX:XX:XX:XX:XX:XX )其中,前三个字节是由IEEE的注册管理机构RA负责给不同厂家分配的代码(高位24位),也称为“编制上唯一的标识符”(Organizationally Unique Identifier),后三个字节(低位24位)由各厂家自行指派给生产的适配器接口,称为扩展标识符(唯一性)。

三、历程

当产品部提出wifi考勤打卡需求时,普遍认为一个路由器有一个mac地址,手机连接wifi可以根据mac地址等信息进行打卡。当我们用多个手机连接公司名称为A(SSID)的wifi时,发现手机上展示的mac地址并不是一致的,这个就尴尬了,打翻了原有理念。
然后发现我们公司共有五个无线路由器,wifi名称都是A。哦,这时候才感觉到原来以前的知识还是靠谱的,可能是多个手机具体连接的路由器不是同一个。

然后把五个路由器wifi热点名称改为A、B、C、D、E,多个手机连接A热点时,发现手机得到的mac地址是一致的,到这里可以得出的结论是手机连接同一个wifi热点得到的mac地址是一致的。但是…..又尴尬了。

当多款手机连接B热点时,发现又出现了不一致的mac地址,查找原因发现,原来B无线路由器中可以设置2.4G Hz和5G Hz两个不同频段的wifi热点。B路由器中默认是开启2.4G Hz和5G Hz频段的wifi热点,并且wifi名称(SSID)是同一个。经过检查还有个问题是B路由器的出厂mac地址和手机连接得到的mac地址不一致。

例如上面举得例子:笔者公司的某款无线路由器B的出厂MAC地址为 XX:XX:XX:XX:XX:F1,当手机连接此wifi查看mac地址时发现是XX:XX:XX:XX:XX:F2,另一款手机连接时是XX:XX:XX:XX:XX:F3。由此可得出的结论是,路由器有一个基准(出厂)mac地址,然后发射出wifi的mac在基准mac地址上按照一定的算法进行变动,具体的变动算法不清楚,有清楚的请告知我,非常感谢。

另外还有一个问题是,C路由器设备后面所写的出厂说明mac地址是XX:XX:XX:XX:XX:56,但是通过路由器后台看到的出厂mac地址是XX:XX:XX:XX:XX:57,手机连接后得到的mac地址是XX:XX:XX:XX:XX:56。这就尴尬了,是厂家写错了还是根据特定的算法算的?

除了根据wifi设备分析外,我们也对具有wifi考勤打卡功能的软件进行了分析。比如现在比较火爆的由阿里团队研发的钉钉,以及纷享销客APP,在Android端,他们的处理都是获取周围wifi信息(并不是当前手机连接的wifi)进行打卡。在iOS端,他们的处理都是根据当前手机连接的wifi信息进行打卡。据iOS同事说,iOS获取周围wifi信息需要申请此功能,并最低支持版本是iOS 9。另外据可靠消息,分享逍客对mac地址的处理也是通过忽略低4位进行匹配。

四、结论

经过上述分析,手机获取的无线路由器MAC地址的低4位是变化的。那我们实现这个需求时,除了匹配虚拟位置、手机信息、wifi相关等其他信息外,只针对mac地址,我们可以忽略mac地址的低4位来做匹配。

五、参考资料

http://www.juniper.net/documentation/en_US/junos-space-apps12.3/network-director/topics/concept/wireless-ssid-bssid-essid.html 


qrcode_for_gh_08bfa7313fb2_258.jpg

微信公众号:IT大前端
关注可了解更多的大前端领域技术 收起阅读 »

2019年3月3日凌晨阿里云宕机引发事故公告

环信的小伙伴,您好,

        首先,环信对2019年3月3日凌晨的事故为您造成的影响深表歉意,故障公告如下:

事故概述:

       3月2日晚23:55,阿里云华北2地域ECS出现了大面积宕机,导致使用这些服务的环信即时通讯云北京集群和VIP6集群服务不可用。
 

微信图片_20190304154115.jpg

(阿里云事故公告)



环信客服云的用户,如果添加了受影响的IM集群上的APP Key作为APP渠道接入的关联并且使用环信IM SDK,也会出现收发消息失败的现象。

使用系统默认的APP渠道接入关联,支持第二通道的客户互动云访客端SDK和网页端的环信客户互动云用户均不受影响。

处理过程:
  • 环信运维团队监控到异常后,第一时间联系了阿里云,在阿里云ECS服务全面恢复之前,利用少量可用的实例提前进行了服务恢复
  • 3月3日凌晨3:10,环信即时通讯云的北京集群和VIP6集群服务全部恢复


后续改进:
  • 我们会同阿里云一起排查事故原因,防止此类事故再次发生,对您造成的影响深表歉意,感谢您对环信的支持。

继续阅读 »
环信的小伙伴,您好,

        首先,环信对2019年3月3日凌晨的事故为您造成的影响深表歉意,故障公告如下:

事故概述:

       3月2日晚23:55,阿里云华北2地域ECS出现了大面积宕机,导致使用这些服务的环信即时通讯云北京集群和VIP6集群服务不可用。
 

微信图片_20190304154115.jpg

(阿里云事故公告)



环信客服云的用户,如果添加了受影响的IM集群上的APP Key作为APP渠道接入的关联并且使用环信IM SDK,也会出现收发消息失败的现象。

使用系统默认的APP渠道接入关联,支持第二通道的客户互动云访客端SDK和网页端的环信客户互动云用户均不受影响。

处理过程:
  • 环信运维团队监控到异常后,第一时间联系了阿里云,在阿里云ECS服务全面恢复之前,利用少量可用的实例提前进行了服务恢复
  • 3月3日凌晨3:10,环信即时通讯云的北京集群和VIP6集群服务全部恢复


后续改进:
  • 我们会同阿里云一起排查事故原因,防止此类事故再次发生,对您造成的影响深表歉意,感谢您对环信的支持。

收起阅读 »

.net快速开发平台,learun敏捷开发框架




前言:

快速开发的趋势

 在十年前,没有人会想到互联网会发展成今天这个样子,同样,也没有人料到软件开发行业也会经历如此大的巨变,在开发这一行业,停下学习就等于死亡并不是危言耸听,不关注行业未来发展趋势的人可能错过了第一个十年,如果不学习,恐怕第二个也要错过了。

快速开发目前风头正盛,但是十分完善的快速开发平台目前并不多,用过的可能都知道,虽然宣称可以覆盖各种功能,但实际使用起来bug也少不到哪里去,之所以越来越受到人们的关注,是因为它能提供便捷化、个性化的软件开发服务。
所谓快速开发其实是针对标准开发而言,通俗的讲,快速开发平台其实是一套软件半成品加一套功能3D打印机,相当于一座建好的毛坯房,主体框架已经建好,样板已经做好,各类装修材料也已经准备齐全,业务功能可以通过3D打印机生成,用户可以在这个框架以不写代码或少些代码的方式进行业务系统的开发工作。

快速开发平台在中国的发展历程不算长,但是却很迅速,而在西方国家,这一开发模式已经在各种企业中广泛应用,占据了近一半的市场份额。这一模式的好处是软件可变性强,业务延展性好,对供需双方来说付出的成本都要小很多。

而我国,在近些年,经济才开始突飞猛进,由于特殊的社会与经济环境,这种半定制的软件平台还是一种新生事物,没有被大多数人充分理解。但是实际上,每一个企业由于自身所处的行业不同,历史背景及业务状况不同,对软件系统会有不同的特殊要求,尤其是现在的一些互联网企业往往提供的都是一些个性化的服务,其对软件的实际需求可能五花八门,显然市场上的通用软件不可能全部兼顾的到,这就会对公司的实际运作造成影响,同时,对通用标准软件的口碑也造成一定的影响。

快速开发平台的出现就是为了解决软件在企业中水土不服的情况,虽然目前国内已经有多家公司在此领域进行布局,但时至今日,依然没有一家领军企业的出现,这涉及到多方面原因,开发公司自身来说,规模与技术实力有限;社会因素来说是企业自身管理需求。比如,早些年的劳动密集型产业,软件个性化需求不够迫切,类excel服务器平台基本上就解决了问题,不需要太高的灵活性。但是随着

中国劳动力成本优势的丧失、国民素质的提升、个性化互联网公司的井喷,快速开发平台将在中国迎来一个高速发展期。
Learun快速开发平台简介

Learun快速开发平台是一套基于智能化可扩展组件式的软件系统项目,使用了当前主流的应用开发技术,框架内置工作流、向导式智能开发组件、即时通讯组件、APP开发组件、微信组件、通用权限等一系列组件,以及可扩展的系统机制,开发人员通过一系列简单配置就可以快速构建高质量的信息系统。

UI:至美至简,多风格可选
经典版
炫动版
飞扬版
风尚版

 

站在技术前沿,learun能解决什么

 一、提高开发效率

整体框架都已经搭建好了,开发者只需要实现业务功能。并且框架内已经集成了大量业务模板,大量的公共组件,开发人员只需要根据开发向导进行设置就可快速完成开发工作。比起传统的开发至少要节约90%的工作量,能够大大地提升开发效率。

二、提升软件质量

规范的编码,专业的架构,稳定高效的底层。这是软件质量的先天优势。基于learun敏捷开发框架做开发,可以使软件质量大幅提升。

三、降低成本

本身在提高效率的同时就是在降低成本。现在软件工程师的工资一般都比较高,特别是架构师级别的动不动就数十万年薪,使得软件开发的成本变得非常之高。在使用learun敏捷开发框架的条件下,初级程序员甚至只要思路清晰的人就可以进行功能开发。开发周期变短,对开发人员的要求变低,也使得开发成本大幅下降。

四、提高客户满意度

Learun为开发人员提供了美观简洁的UI,美观大方、操作便捷,用户体验友好度高。

五、稳定高效的技术支持团队

维护期内learun开发团队原班人马为会提供优质贴心的技术支持,不管是架构还是编码都能全方位的贴心服务,不用担心开发过程中遇到的阻力,免去了因员工流失而给软件项目带来的各种损失。

六、提供框架源代码,提供完整的授权

框架提供全部源代码,毫不保留。二次开发出售无需授权,毫无后顾之忧。

 

Learun能开发什么

一、业务管理软件

ERP、MIS、CRM、WMS、MES、TMS、物流快递管理等这类企业管理系统已经被几家大的软件公司产品化,但是每个行业都有不同的业务需求,每家企业都会有自己不同的业务需求。

标准品无法做到面面俱到的所以我们很难采购到自己想要的产品。独立从头到尾开发一套系统需要大量的人力物力,到头来成本可能比采购软件成品还高,力软敏捷开发框架已经为开发都搭好框架预置了各类基础模块可以直接使用,另外系统根据各类系统的特点建立了多套开发模板,开发者可以按照开发向导快速开发出各种业务系统。

二、协同办公软件

Learun敏捷开发框架已经内置了工作流引擎、自定义表单引擎、即时通讯模块再配合框架完善的权限管理模块,可以轻松地定制协同办公软件,OA、HRM、KM等系统的开发将变得非常简单甚至不需要编写一行代码。

三、电商平台后台

Learun敏捷开发框架强大的后台管理功能及微信模块、短信平台模块开发电商平台后台也非常方便。

四、商业智能(BI)软件

Learun框架集成了大量图表插件,并且提供了智能图表功能,开发者只需要按照向导操作就能生成图形报表。所以此框架也非常适合开发BI软件。




learun功能分布详情

Learun以“让开发变得简单”为宗旨,部署有完善的基础功能

一、系统管理


二、单位组织





三、表单中心




四、工作流程




五、报表中心







 

六、公共信息




七、常用示例




字典分类表




可视化开发


内置代码生成器,只需点击下一步,所有部署自动完成



插件及拓展

框架搭配众多的插件及拓展功能,均支持当前主流浏览器,基本可以满足任何需求



 版本更迭

learun快速开发平台是一款不断成长的敏捷开发框架,经过不断的版本更迭,目前已经更新至7.0版本,需要体验或升级的客户,请至官网www.learun.cn操作。

目前网络上存在的一些盗版软件,均非力软官方发布,不能享受力软持续的技术指导,使用有风险,请谨慎辨别,支持正版。






力软敏捷开发框架7.0 版本发布

2018年08月01日



1.多语言功能;

2.代码生成器模版;

a.可编辑列表代码生成器(Excel风格)模版;

b.报表现实代码生成器模版;

3.树形代码生成功能;

4.动态配置首页功能;

5.外部邮件收发功能;

6.办公类型文件在线预览功能;

7.表单页面的弹出框;

a.左边树;

b.中间选择;

c.右边显示已选择;



1.表格控件子表格展开显示异常问题;

2.日期控件偶尔出现格式错乱问题;

3.分页控件页面再次加载页数错误问题;



1.代码生成器优化成拖拽式设计;

2.支持数据库多架构设计;

3.表格组件支持;

a.下拉框;

b.单选框;

c.复选框;

d.弹窗等功能;

4.工作流支持动态选择下一审批人;

5.IM组件重构;

6.文件上传效率;

7.工作流审核方式;

8.重新美化四套皮肤;




力软敏捷开发框架6.1.6.2 版本发布

2018年04月03日



1.手机流程

a.我的流程- 可查看流程进度和表单内容;

b.待办任务- 可查看流程进度和表单内容,审核;

c.已办任务- 可查看流程进度和表单内容;

d.自定义表单流程发起审核;

2.自定义表单可以发布到手机端;

3.数据权限-增加上下级数据权限管理;

4.新增在线建表功能;

5.一套APP开发实例;



1.pc端流程-修复传阅节点bug;

2.数据库事务中查询异常bug;

3.文件上传控件兼容性bug;

4.Oracle数据库流程流转中bug;

5.Oracle数据库自定义表单显示中的bug;



1.手机端用户密码修改功能;

2.pc端优化了经典版皮肤;

3.pc端流程;

a.增加流程时间轴;

b.下一节点若多人可审核,审核时可具体指定某一人;

4.手机端支持从vs2017进行开发、打包;

5.数据库连接性能优化;

6.前端基础数据加载优化;



相信,随着敏捷思想的不断深入,力软敏捷开发框架会得到越来越多人的认同,毕竟,价值才是第一驱动力。

一路走来数个年头,感谢力软敏捷开发框架框的支持者与使用者,大家可以通过下面的地址了解详情。

力软敏捷开发框架官方网站:www.learun.cn

力软敏捷开发框架官方免费体验网站:http://www.learun.cn/Home/VerificationForm

力软敏捷开发框架由专业团队长期打造、一直在更新、一直在升级,请放心使用!
继续阅读 »



前言:

快速开发的趋势

 在十年前,没有人会想到互联网会发展成今天这个样子,同样,也没有人料到软件开发行业也会经历如此大的巨变,在开发这一行业,停下学习就等于死亡并不是危言耸听,不关注行业未来发展趋势的人可能错过了第一个十年,如果不学习,恐怕第二个也要错过了。

快速开发目前风头正盛,但是十分完善的快速开发平台目前并不多,用过的可能都知道,虽然宣称可以覆盖各种功能,但实际使用起来bug也少不到哪里去,之所以越来越受到人们的关注,是因为它能提供便捷化、个性化的软件开发服务。
所谓快速开发其实是针对标准开发而言,通俗的讲,快速开发平台其实是一套软件半成品加一套功能3D打印机,相当于一座建好的毛坯房,主体框架已经建好,样板已经做好,各类装修材料也已经准备齐全,业务功能可以通过3D打印机生成,用户可以在这个框架以不写代码或少些代码的方式进行业务系统的开发工作。

快速开发平台在中国的发展历程不算长,但是却很迅速,而在西方国家,这一开发模式已经在各种企业中广泛应用,占据了近一半的市场份额。这一模式的好处是软件可变性强,业务延展性好,对供需双方来说付出的成本都要小很多。

而我国,在近些年,经济才开始突飞猛进,由于特殊的社会与经济环境,这种半定制的软件平台还是一种新生事物,没有被大多数人充分理解。但是实际上,每一个企业由于自身所处的行业不同,历史背景及业务状况不同,对软件系统会有不同的特殊要求,尤其是现在的一些互联网企业往往提供的都是一些个性化的服务,其对软件的实际需求可能五花八门,显然市场上的通用软件不可能全部兼顾的到,这就会对公司的实际运作造成影响,同时,对通用标准软件的口碑也造成一定的影响。

快速开发平台的出现就是为了解决软件在企业中水土不服的情况,虽然目前国内已经有多家公司在此领域进行布局,但时至今日,依然没有一家领军企业的出现,这涉及到多方面原因,开发公司自身来说,规模与技术实力有限;社会因素来说是企业自身管理需求。比如,早些年的劳动密集型产业,软件个性化需求不够迫切,类excel服务器平台基本上就解决了问题,不需要太高的灵活性。但是随着

中国劳动力成本优势的丧失、国民素质的提升、个性化互联网公司的井喷,快速开发平台将在中国迎来一个高速发展期。
Learun快速开发平台简介

Learun快速开发平台是一套基于智能化可扩展组件式的软件系统项目,使用了当前主流的应用开发技术,框架内置工作流、向导式智能开发组件、即时通讯组件、APP开发组件、微信组件、通用权限等一系列组件,以及可扩展的系统机制,开发人员通过一系列简单配置就可以快速构建高质量的信息系统。

UI:至美至简,多风格可选
经典版
炫动版
飞扬版
风尚版

 

站在技术前沿,learun能解决什么

 一、提高开发效率

整体框架都已经搭建好了,开发者只需要实现业务功能。并且框架内已经集成了大量业务模板,大量的公共组件,开发人员只需要根据开发向导进行设置就可快速完成开发工作。比起传统的开发至少要节约90%的工作量,能够大大地提升开发效率。

二、提升软件质量

规范的编码,专业的架构,稳定高效的底层。这是软件质量的先天优势。基于learun敏捷开发框架做开发,可以使软件质量大幅提升。

三、降低成本

本身在提高效率的同时就是在降低成本。现在软件工程师的工资一般都比较高,特别是架构师级别的动不动就数十万年薪,使得软件开发的成本变得非常之高。在使用learun敏捷开发框架的条件下,初级程序员甚至只要思路清晰的人就可以进行功能开发。开发周期变短,对开发人员的要求变低,也使得开发成本大幅下降。

四、提高客户满意度

Learun为开发人员提供了美观简洁的UI,美观大方、操作便捷,用户体验友好度高。

五、稳定高效的技术支持团队

维护期内learun开发团队原班人马为会提供优质贴心的技术支持,不管是架构还是编码都能全方位的贴心服务,不用担心开发过程中遇到的阻力,免去了因员工流失而给软件项目带来的各种损失。

六、提供框架源代码,提供完整的授权

框架提供全部源代码,毫不保留。二次开发出售无需授权,毫无后顾之忧。

 

Learun能开发什么

一、业务管理软件

ERP、MIS、CRM、WMS、MES、TMS、物流快递管理等这类企业管理系统已经被几家大的软件公司产品化,但是每个行业都有不同的业务需求,每家企业都会有自己不同的业务需求。

标准品无法做到面面俱到的所以我们很难采购到自己想要的产品。独立从头到尾开发一套系统需要大量的人力物力,到头来成本可能比采购软件成品还高,力软敏捷开发框架已经为开发都搭好框架预置了各类基础模块可以直接使用,另外系统根据各类系统的特点建立了多套开发模板,开发者可以按照开发向导快速开发出各种业务系统。

二、协同办公软件

Learun敏捷开发框架已经内置了工作流引擎、自定义表单引擎、即时通讯模块再配合框架完善的权限管理模块,可以轻松地定制协同办公软件,OA、HRM、KM等系统的开发将变得非常简单甚至不需要编写一行代码。

三、电商平台后台

Learun敏捷开发框架强大的后台管理功能及微信模块、短信平台模块开发电商平台后台也非常方便。

四、商业智能(BI)软件

Learun框架集成了大量图表插件,并且提供了智能图表功能,开发者只需要按照向导操作就能生成图形报表。所以此框架也非常适合开发BI软件。




learun功能分布详情

Learun以“让开发变得简单”为宗旨,部署有完善的基础功能

一、系统管理


二、单位组织





三、表单中心




四、工作流程




五、报表中心







 

六、公共信息




七、常用示例




字典分类表




可视化开发


内置代码生成器,只需点击下一步,所有部署自动完成



插件及拓展

框架搭配众多的插件及拓展功能,均支持当前主流浏览器,基本可以满足任何需求



 版本更迭

learun快速开发平台是一款不断成长的敏捷开发框架,经过不断的版本更迭,目前已经更新至7.0版本,需要体验或升级的客户,请至官网www.learun.cn操作。

目前网络上存在的一些盗版软件,均非力软官方发布,不能享受力软持续的技术指导,使用有风险,请谨慎辨别,支持正版。






力软敏捷开发框架7.0 版本发布

2018年08月01日



1.多语言功能;

2.代码生成器模版;

a.可编辑列表代码生成器(Excel风格)模版;

b.报表现实代码生成器模版;

3.树形代码生成功能;

4.动态配置首页功能;

5.外部邮件收发功能;

6.办公类型文件在线预览功能;

7.表单页面的弹出框;

a.左边树;

b.中间选择;

c.右边显示已选择;



1.表格控件子表格展开显示异常问题;

2.日期控件偶尔出现格式错乱问题;

3.分页控件页面再次加载页数错误问题;



1.代码生成器优化成拖拽式设计;

2.支持数据库多架构设计;

3.表格组件支持;

a.下拉框;

b.单选框;

c.复选框;

d.弹窗等功能;

4.工作流支持动态选择下一审批人;

5.IM组件重构;

6.文件上传效率;

7.工作流审核方式;

8.重新美化四套皮肤;




力软敏捷开发框架6.1.6.2 版本发布

2018年04月03日



1.手机流程

a.我的流程- 可查看流程进度和表单内容;

b.待办任务- 可查看流程进度和表单内容,审核;

c.已办任务- 可查看流程进度和表单内容;

d.自定义表单流程发起审核;

2.自定义表单可以发布到手机端;

3.数据权限-增加上下级数据权限管理;

4.新增在线建表功能;

5.一套APP开发实例;



1.pc端流程-修复传阅节点bug;

2.数据库事务中查询异常bug;

3.文件上传控件兼容性bug;

4.Oracle数据库流程流转中bug;

5.Oracle数据库自定义表单显示中的bug;



1.手机端用户密码修改功能;

2.pc端优化了经典版皮肤;

3.pc端流程;

a.增加流程时间轴;

b.下一节点若多人可审核,审核时可具体指定某一人;

4.手机端支持从vs2017进行开发、打包;

5.数据库连接性能优化;

6.前端基础数据加载优化;



相信,随着敏捷思想的不断深入,力软敏捷开发框架会得到越来越多人的认同,毕竟,价值才是第一驱动力。

一路走来数个年头,感谢力软敏捷开发框架框的支持者与使用者,大家可以通过下面的地址了解详情。

力软敏捷开发框架官方网站:www.learun.cn

力软敏捷开发框架官方免费体验网站:http://www.learun.cn/Home/VerificationForm

力软敏捷开发框架由专业团队长期打造、一直在更新、一直在升级,请放心使用! 收起阅读 »

老司机带你一文读懂Android运行时权限

 
老司机发车了,未到终点请勿下车. 嘟嘟嘟~~~
 运行时权限从Android 6.0版本开始的,如果你的项目中 targetSdkVersion/compileSdkVersion 大于等于23,那么你就必须要考虑动态权限了。

权限又分为普通权限和危险权限。
普通权限如下:
android.permission.ACCESS LOCATIONEXTRA_COMMANDS 
android.permission.ACCESS NETWORKSTATE
android.permission.ACCESS NOTIFICATIONPOLICY
android.permission.ACCESS WIFISTATE
android.permission.ACCESS WIMAXSTATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE NETWORKSTATE
android.permission.CHANGE WIFIMULTICAST_STATE
android.permission.CHANGE WIFISTATE
android.permission.CHANGE WIMAXSTATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND STATUSBAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET PACKAGESIZE
android.permission.INTERNET
android.permission.KILL BACKGROUNDPROCESSES
android.permission.MODIFY AUDIOSETTINGS
android.permission.NFC
android.permission.READ SYNCSETTINGS
android.permission.READ SYNCSTATS
android.permission.RECEIVE BOOTCOMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST INSTALLPACKAGES
android.permission.SET TIMEZONE
android.permission.SET_WALLPAPER
android.permission.SET WALLPAPERHINTS
android.permission.SUBSCRIBED FEEDSREAD
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE SYNCSETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT

普通权限是当需要用到时,只需要在清单文件中声明就可。危险权限除了需要在清单文件中声明外,还需在代码中动态进行判断申请。

危险权限如下:
android.permission-group.CALENDAR   
android.permission.READ_CALENDAR
android.permission.WRITE_CALENDAR

android.permission-group.CAMERA
android.permission.CAMERA

android.permission-group.CONTACTS
android.permission.READ_CONTACTS
android.permission.WRITE_CONTACTS
android.permission.GET_ACCOUNTS

android.permission-group.LOCATION
android.permission.ACCESS_FINE_LOCATION
android.permission.ACCESS_COARSE_LOCATION

android.permission-group.MICROPHONE
android.permission.RECORD_AUDIO

android.permission-group.PHONE
android.permission.READ_PHONE_STATE
android.permission.CALL_PHONE
android.permission.READ_CALL_LOG
android.permission.WRITE_CALL_LOG
com.android.voicemail.permission.ADD_VOICEMAIL
android.permission.USE_SIP
android.permission.PROCESS_OUTGOING_CALLS

android.permission-group.SENSORS
android.permission.BODY_SENSORS

android.permission-group.SMS
android.permission.SEND_SMS
android.permission.RECEIVE_SMS
android.permission.READ_SMS
android.permission.RECEIVE_WAP_PUSH
android.permission.RECEIVE_MMS
android.permission.READ_CELL_BROADCASTS

android.permission-group.STORAGE
android.permission.READ_EXTERNAL_STORAGE
android.permission.WRITE_EXTERNAL_STORAGE

危险权限是按组划分的,每一组中当某一个权限被允许或者拒绝后,同一组的其他权限也相应的自动允许或者拒绝。

当targetSdkVersion/compileSdkVersion大于等于23时,我们用到危险权限时,应按照这样的逻辑去处理。先判断当前应用是否具有权限,如果没有就去申请,当用户允许或者拒绝后,会回调相应的方法,我们在回调中处理自己的逻辑。


申请单个权限

在Activity/Fragment中,判断是否具有权限的方法是 checkSelfPermission(),申请权限的方法是requestPermissions(),然后用户允许或者拒绝后会回调方法onRequestPermissionsResult()。
虽然Activity/Fragment提供了这些方法,如果我们用这些的话,要判断安卓版本是否大于等于23,代码如下: 
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
//动态请求权限

}else{
//直接去调用代码
}

 
谷歌工程师当然考虑到了这些,于是给开发者提供了兼容包,在Activity/Fragment中用ContextCompat.checkSelfPermission()判断是否具有权限;
在Activity中用 ActivityCompat.requestPermissions()请求权限;
在Fragment中直接用 requestPermissions()请求权限,不要在前面加上ActivityCompat,否则会回调Fragment所在Activity的回调方法;
在Activity/Fragment中用户允许或者拒绝权限后会回调onRequestPermissionsResult()方法。

下面看一个平常的调用系统相机拍照功能的代码:
private void takePhoto() {
Intent photoIn = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(photoIn, TAKE_PHOTO_REQUEST);
}

 
如果我们项目targetSdkVersion/compileSdkVersion大于等于23,那么就需要动态申请权限了:
if (ContextCompat.checkSelfPermission(MySetupActivity.this,  Manifest.permission.CAMERA)!=  PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MySetupActivity.this,new String[]{Manifest.permission.CAMERA}, 100);
} else {
takePhoto();
}

 
先判断是否具有权限,如果有直接去执行相关代码,没有则去申请权限。
当用户点击允许或者拒绝权限时,会回调onRequestPermissionsResult方法,我们在此方法中进行处理:
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {

if (requestCode == 100) {//相机
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
takePhoto();
} else {
// Permission Denied
AlertDialog mDialog = new AlertDialog.Builder(MySetupActivity.this)
.setTitle("友好提醒")
.setMessage("您已拒绝权限,请开启权限!")
.setPositiveButton("开启", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
ShowAppSetDetails.showInstalledAppDetails(MySetupActivity.this, "user.zhuku.com");
LogPrint.logILsj(TAG, "开启权限设置");
}
})
.setCancelable(true)
.create();
mDialog.show();
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

当用户允许权限后我们直接执行相关代码,若拒绝则提示用户,弹窗提示是否需要开启权限。其中的
ShowAppSetDetails.showInstalledAppDetails(MySetupActivity.this, "user.zhuku.com");

是开启当前app信息设置界面的代码。

具体代码如下:
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
public class ShowAppSetDetails {
private static final String SCHEME = "package";
/**
* 调用系统InstalledAppDetails界面所需的Extra名称(用于Android 2.1及之前版本)
*/
private static final String APP_PKG_NAME_21 = "com.android.settings.ApplicationPkgName";
/**
* 调用系统InstalledAppDetails界面所需的Extra名称(用于Android 2.2)
*/
private static final String APP_PKG_NAME_22 = "pkg";
/**
* InstalledAppDetails所在包名
*/
private static final String APP_DETAILS_PACKAGE_NAME = "com.android.settings";
/**
* InstalledAppDetails类名
*/
private static final String APP_DETAILS_CLASS_NAME = "com.android.settings.InstalledAppDetails";

/**
* 调用系统InstalledAppDetails界面显示已安装应用程序的详细信息。 对于Android 2.3(Api Level
* 9)以上,使用SDK提供的接口; 2.3以下,使用非公开的接口(查看InstalledAppDetails源码)。
*
* @param context
* @param packageName 应用程序的包名
*/
public static void showInstalledAppDetails(Context context, String packageName) {
Intent intent = new Intent();
final int apiLevel = Build.VERSION.SDK_INT;
if (apiLevel >= 9) { // 2.3(ApiLevel 9)以上,使用SDK提供的接口
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts(SCHEME, packageName, null);
intent.setData(uri);
} else { // 2.3以下,使用非公开的接口(查看InstalledAppDetails源码)
// 2.2和2.1中,InstalledAppDetails使用的APP_PKG_NAME不同。
final String appPkgName = (apiLevel == 8 ? APP_PKG_NAME_22
: APP_PKG_NAME_21);
intent.setAction(Intent.ACTION_VIEW);
intent.setClassName(APP_DETAILS_PACKAGE_NAME,
APP_DETAILS_CLASS_NAME);
intent.putExtra(appPkgName, packageName);
}
context.startActivity(intent);
}
}

当app申请权限时,如果用户点击了“不再提醒”,则会直接回调拒绝权限,为此谷歌工程师提供了shouldShowRequestPermissionRationale()方法。兼容包中方法是ActivityCompat.shouldShowRequestPermissionRationale(),如果是在Fragment中使用请直接用shouldShowRequestPermissionRationale()。

ActivityCompat.shouldShowRequestPermissionRationale()方法返回值是boolean类型,当第一次申请权限时,此方法会返回false,如果用户点击“不再提醒”,则此方法会返回true。
 
详细代码如下:
if (ContextCompat.checkSelfPermission(MySetupActivity.this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(MySetupActivity.this, Manifest.permission.CAMERA)) {
//已经禁止提示了
mDialog = new AlertDialog.Builder(MySetupActivity.this)
.setTitle("友好提醒")
.setMessage("您已拒绝相机权限,此功能需要开启,是否开启?")
.setPositiveButton("是", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
ActivityCompat.requestPermissions(MySetupActivity.this,
new String[]{Manifest.permission.CAMERA},
100);
}
})
.setNegativeButton("否", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
})
.setCancelable(true)
.create();
mDialog.show();
} else {
ActivityCompat.requestPermissions(MySetupActivity.this,
new String[]{Manifest.permission.CAMERA},
100);
}

} else {
takePhoto();
}

 
如果是在Fragment中,则代码如下:
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
//已经禁止提示了
mDialog = new AlertDialog.Builder(getContext())
.setTitle("友好提醒")
.setMessage("您已拒绝相机权限,此功能需要开启,是否开启?")
.setPositiveButton("是", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
requestPermissions(new String[]{Manifest.permission.CAMERA},
PERMISSIONS_CAMERA);
}
})
.setNegativeButton("否", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
})
.setCancelable(true)
.create();
mDialog.show();
} else {
requestPermissions(new String[]{Manifest.permission.CAMERA},
PERMISSIONS_CAMERA);
}

} else {
selectPicFromCamera();
}

特别要注意的是,如果在Fragment中请求权限,若在Activity中也重写了onRequestPermissionsResult(),则onRequestPermissionsResult()方法中一定要写
  super.onRequestPermissionsResult(requestCode, permissions, grantResults);

这句代码,若不写,则Activity不会分发执行Fragment中的权限回调方法。

因为Fragment中requestPermissions()源码如下:
public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
if (mHost == null) {
throw new IllegalStateException("Fragment " + this + " not attached to Activity");
}
mHost.onRequestPermissionsFromFragment(this, permissions, requestCode);
}

其实在Fragment请求权限也是在它Activity中请求,只是把回调结果传递给了Fragment。


一次申请多个权限

例如需要申请的权限如下:
    /**
* 需要进行检测的权限数组
*/
protected String[] permissionList = {
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_LOCATION_EXTRA_COMMANDS,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
};

 
我们要先判断每一个权限是否已经允许或者拒绝,当有某一个权限未被允许时,则申请未被允许的权限。
protected void onStart() {
super.onStart();

if (PermissionUtils.checkSelfPermission(SplashActivity.this, permissionList)) {
PermissionUtils.checkPermissions(this, 0, permissionList);
} else {
//处理业务逻辑
}

}

其中PermissionUtils类的代码如下:
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;

import java.util.ArrayList;
import java.util.List;

public class PermissionUtils {
/**
* 检查权限
*/
public static void checkPermissions(Activity activity, int permissRequestCode, String... permissions) {
List<String> needRequestPermissonList = findDeniedPermissions(activity, permissions);
if (null != needRequestPermissonList
&& needRequestPermissonList.size() > 0) {
ActivityCompat.requestPermissions(activity,
needRequestPermissonList.toArray(
new String[needRequestPermissonList.size()]),
permissRequestCode);
}
}

/**
* 获取权限中需要申请权限的列表
*/
public static List<String> findDeniedPermissions(Activity activity, String[] permissions) {
List<String> needRequestPermissonList = new ArrayList<String>();
for (String perm : permissions) {
if (ContextCompat.checkSelfPermission(activity,
perm) != PackageManager.PERMISSION_GRANTED) {
needRequestPermissonList.add(perm);
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(
activity, perm)) {
needRequestPermissonList.add(perm);
}
}
}
return needRequestPermissonList;
}

public static boolean checkSelfPermission(Context context, String[] permissions) {
for (String perm : permissions) {
if (ContextCompat.checkSelfPermission(context, perm) != PackageManager.PERMISSION_GRANTED) {
return true;
}
}
return false;
}

public static boolean checkSelfResult(int[] grantResults) {
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
}

 
onRequestPermissionsResult回调方法中则这样处理:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);

if (requestCode == 0) {
if (PermissionUtils.checkSelfResult(grantResults)) {
// Permission Granted
//处理业务逻辑
} else {
// Permission Denied

if (null == mDialog)
mDialog = new AlertDialog.Builder(SplashActivity.this)
.setTitle("友好提醒")
.setMessage("没有权限将不能更好的使用,请开启权限!")
.setPositiveButton("开启", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
ShowAppSetDetails.showInstalledAppDetails(SplashActivity.this, "user.zhuku.com");
LogPrint.logILsj(TAG, "开启权限设置");
}
})
.setCancelable(false)
.create();

if (!mDialog.isShowing()) {
mDialog.show();
}
}
}
}

 
如果项目需要在页面可见时进行权限申请,请放在onStart()方法中,不要写在onResume()中。
可以想象一下,如果写在onResume()中,当用户同意了权限,则无碍,若是点击拒绝,则会回调权限拒绝的方法,这时系统申请权限的对话框消失不见,会再次调用onResume()请求权限显示对话框,若是还拒绝,则会再次调用onResume()方法,一直处于死循环。
因为系统请求权限的对话框其实一个开启了一个Activity。部分源码如下:  
 
 public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
if (mHasCurrentPermissionsRequest) {
Log.w(TAG, "Can reqeust only one set of permissions at a time");
// Dispatch the callback with empty arrays which means a cancellation.
onRequestPermissionsResult(requestCode, new String[0], new int[0]);
return;
}
Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null);
mHasCurrentPermissionsRequest = true;
}

至此,老司机本次发车已到终点,这一程体验,你还不会封装自己的运行时权限库吗?
 
 

qrcode_for_gh_08bfa7313fb2_258.jpg

微信公众号:IT大前端
关注可了解更多的大前端领域技术
继续阅读 »
 
老司机发车了,未到终点请勿下车. 嘟嘟嘟~~~
 运行时权限从Android 6.0版本开始的,如果你的项目中 targetSdkVersion/compileSdkVersion 大于等于23,那么你就必须要考虑动态权限了。

权限又分为普通权限和危险权限。
普通权限如下:
android.permission.ACCESS LOCATIONEXTRA_COMMANDS 
android.permission.ACCESS NETWORKSTATE
android.permission.ACCESS NOTIFICATIONPOLICY
android.permission.ACCESS WIFISTATE
android.permission.ACCESS WIMAXSTATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE NETWORKSTATE
android.permission.CHANGE WIFIMULTICAST_STATE
android.permission.CHANGE WIFISTATE
android.permission.CHANGE WIMAXSTATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND STATUSBAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET PACKAGESIZE
android.permission.INTERNET
android.permission.KILL BACKGROUNDPROCESSES
android.permission.MODIFY AUDIOSETTINGS
android.permission.NFC
android.permission.READ SYNCSETTINGS
android.permission.READ SYNCSTATS
android.permission.RECEIVE BOOTCOMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST INSTALLPACKAGES
android.permission.SET TIMEZONE
android.permission.SET_WALLPAPER
android.permission.SET WALLPAPERHINTS
android.permission.SUBSCRIBED FEEDSREAD
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE SYNCSETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT

普通权限是当需要用到时,只需要在清单文件中声明就可。危险权限除了需要在清单文件中声明外,还需在代码中动态进行判断申请。

危险权限如下:
android.permission-group.CALENDAR   
android.permission.READ_CALENDAR
android.permission.WRITE_CALENDAR

android.permission-group.CAMERA
android.permission.CAMERA

android.permission-group.CONTACTS
android.permission.READ_CONTACTS
android.permission.WRITE_CONTACTS
android.permission.GET_ACCOUNTS

android.permission-group.LOCATION
android.permission.ACCESS_FINE_LOCATION
android.permission.ACCESS_COARSE_LOCATION

android.permission-group.MICROPHONE
android.permission.RECORD_AUDIO

android.permission-group.PHONE
android.permission.READ_PHONE_STATE
android.permission.CALL_PHONE
android.permission.READ_CALL_LOG
android.permission.WRITE_CALL_LOG
com.android.voicemail.permission.ADD_VOICEMAIL
android.permission.USE_SIP
android.permission.PROCESS_OUTGOING_CALLS

android.permission-group.SENSORS
android.permission.BODY_SENSORS

android.permission-group.SMS
android.permission.SEND_SMS
android.permission.RECEIVE_SMS
android.permission.READ_SMS
android.permission.RECEIVE_WAP_PUSH
android.permission.RECEIVE_MMS
android.permission.READ_CELL_BROADCASTS

android.permission-group.STORAGE
android.permission.READ_EXTERNAL_STORAGE
android.permission.WRITE_EXTERNAL_STORAGE

危险权限是按组划分的,每一组中当某一个权限被允许或者拒绝后,同一组的其他权限也相应的自动允许或者拒绝。

当targetSdkVersion/compileSdkVersion大于等于23时,我们用到危险权限时,应按照这样的逻辑去处理。先判断当前应用是否具有权限,如果没有就去申请,当用户允许或者拒绝后,会回调相应的方法,我们在回调中处理自己的逻辑。


申请单个权限

在Activity/Fragment中,判断是否具有权限的方法是 checkSelfPermission(),申请权限的方法是requestPermissions(),然后用户允许或者拒绝后会回调方法onRequestPermissionsResult()。
虽然Activity/Fragment提供了这些方法,如果我们用这些的话,要判断安卓版本是否大于等于23,代码如下: 
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
//动态请求权限

}else{
//直接去调用代码
}

 
谷歌工程师当然考虑到了这些,于是给开发者提供了兼容包,在Activity/Fragment中用ContextCompat.checkSelfPermission()判断是否具有权限;
在Activity中用 ActivityCompat.requestPermissions()请求权限;
在Fragment中直接用 requestPermissions()请求权限,不要在前面加上ActivityCompat,否则会回调Fragment所在Activity的回调方法;
在Activity/Fragment中用户允许或者拒绝权限后会回调onRequestPermissionsResult()方法。

下面看一个平常的调用系统相机拍照功能的代码:
private void takePhoto() {
Intent photoIn = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(photoIn, TAKE_PHOTO_REQUEST);
}

 
如果我们项目targetSdkVersion/compileSdkVersion大于等于23,那么就需要动态申请权限了:
if (ContextCompat.checkSelfPermission(MySetupActivity.this,  Manifest.permission.CAMERA)!=  PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MySetupActivity.this,new String[]{Manifest.permission.CAMERA}, 100);
} else {
takePhoto();
}

 
先判断是否具有权限,如果有直接去执行相关代码,没有则去申请权限。
当用户点击允许或者拒绝权限时,会回调onRequestPermissionsResult方法,我们在此方法中进行处理:
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {

if (requestCode == 100) {//相机
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
takePhoto();
} else {
// Permission Denied
AlertDialog mDialog = new AlertDialog.Builder(MySetupActivity.this)
.setTitle("友好提醒")
.setMessage("您已拒绝权限,请开启权限!")
.setPositiveButton("开启", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
ShowAppSetDetails.showInstalledAppDetails(MySetupActivity.this, "user.zhuku.com");
LogPrint.logILsj(TAG, "开启权限设置");
}
})
.setCancelable(true)
.create();
mDialog.show();
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

当用户允许权限后我们直接执行相关代码,若拒绝则提示用户,弹窗提示是否需要开启权限。其中的
ShowAppSetDetails.showInstalledAppDetails(MySetupActivity.this, "user.zhuku.com");

是开启当前app信息设置界面的代码。

具体代码如下:
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
public class ShowAppSetDetails {
private static final String SCHEME = "package";
/**
* 调用系统InstalledAppDetails界面所需的Extra名称(用于Android 2.1及之前版本)
*/
private static final String APP_PKG_NAME_21 = "com.android.settings.ApplicationPkgName";
/**
* 调用系统InstalledAppDetails界面所需的Extra名称(用于Android 2.2)
*/
private static final String APP_PKG_NAME_22 = "pkg";
/**
* InstalledAppDetails所在包名
*/
private static final String APP_DETAILS_PACKAGE_NAME = "com.android.settings";
/**
* InstalledAppDetails类名
*/
private static final String APP_DETAILS_CLASS_NAME = "com.android.settings.InstalledAppDetails";

/**
* 调用系统InstalledAppDetails界面显示已安装应用程序的详细信息。 对于Android 2.3(Api Level
* 9)以上,使用SDK提供的接口; 2.3以下,使用非公开的接口(查看InstalledAppDetails源码)。
*
* @param context
* @param packageName 应用程序的包名
*/
public static void showInstalledAppDetails(Context context, String packageName) {
Intent intent = new Intent();
final int apiLevel = Build.VERSION.SDK_INT;
if (apiLevel >= 9) { // 2.3(ApiLevel 9)以上,使用SDK提供的接口
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts(SCHEME, packageName, null);
intent.setData(uri);
} else { // 2.3以下,使用非公开的接口(查看InstalledAppDetails源码)
// 2.2和2.1中,InstalledAppDetails使用的APP_PKG_NAME不同。
final String appPkgName = (apiLevel == 8 ? APP_PKG_NAME_22
: APP_PKG_NAME_21);
intent.setAction(Intent.ACTION_VIEW);
intent.setClassName(APP_DETAILS_PACKAGE_NAME,
APP_DETAILS_CLASS_NAME);
intent.putExtra(appPkgName, packageName);
}
context.startActivity(intent);
}
}

当app申请权限时,如果用户点击了“不再提醒”,则会直接回调拒绝权限,为此谷歌工程师提供了shouldShowRequestPermissionRationale()方法。兼容包中方法是ActivityCompat.shouldShowRequestPermissionRationale(),如果是在Fragment中使用请直接用shouldShowRequestPermissionRationale()。

ActivityCompat.shouldShowRequestPermissionRationale()方法返回值是boolean类型,当第一次申请权限时,此方法会返回false,如果用户点击“不再提醒”,则此方法会返回true。
 
详细代码如下:
if (ContextCompat.checkSelfPermission(MySetupActivity.this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(MySetupActivity.this, Manifest.permission.CAMERA)) {
//已经禁止提示了
mDialog = new AlertDialog.Builder(MySetupActivity.this)
.setTitle("友好提醒")
.setMessage("您已拒绝相机权限,此功能需要开启,是否开启?")
.setPositiveButton("是", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
ActivityCompat.requestPermissions(MySetupActivity.this,
new String[]{Manifest.permission.CAMERA},
100);
}
})
.setNegativeButton("否", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
})
.setCancelable(true)
.create();
mDialog.show();
} else {
ActivityCompat.requestPermissions(MySetupActivity.this,
new String[]{Manifest.permission.CAMERA},
100);
}

} else {
takePhoto();
}

 
如果是在Fragment中,则代码如下:
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
//已经禁止提示了
mDialog = new AlertDialog.Builder(getContext())
.setTitle("友好提醒")
.setMessage("您已拒绝相机权限,此功能需要开启,是否开启?")
.setPositiveButton("是", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
requestPermissions(new String[]{Manifest.permission.CAMERA},
PERMISSIONS_CAMERA);
}
})
.setNegativeButton("否", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
})
.setCancelable(true)
.create();
mDialog.show();
} else {
requestPermissions(new String[]{Manifest.permission.CAMERA},
PERMISSIONS_CAMERA);
}

} else {
selectPicFromCamera();
}

特别要注意的是,如果在Fragment中请求权限,若在Activity中也重写了onRequestPermissionsResult(),则onRequestPermissionsResult()方法中一定要写
  super.onRequestPermissionsResult(requestCode, permissions, grantResults);

这句代码,若不写,则Activity不会分发执行Fragment中的权限回调方法。

因为Fragment中requestPermissions()源码如下:
public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
if (mHost == null) {
throw new IllegalStateException("Fragment " + this + " not attached to Activity");
}
mHost.onRequestPermissionsFromFragment(this, permissions, requestCode);
}

其实在Fragment请求权限也是在它Activity中请求,只是把回调结果传递给了Fragment。


一次申请多个权限

例如需要申请的权限如下:
    /**
* 需要进行检测的权限数组
*/
protected String[] permissionList = {
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_LOCATION_EXTRA_COMMANDS,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
};

 
我们要先判断每一个权限是否已经允许或者拒绝,当有某一个权限未被允许时,则申请未被允许的权限。
protected void onStart() {
super.onStart();

if (PermissionUtils.checkSelfPermission(SplashActivity.this, permissionList)) {
PermissionUtils.checkPermissions(this, 0, permissionList);
} else {
//处理业务逻辑
}

}

其中PermissionUtils类的代码如下:
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;

import java.util.ArrayList;
import java.util.List;

public class PermissionUtils {
/**
* 检查权限
*/
public static void checkPermissions(Activity activity, int permissRequestCode, String... permissions) {
List<String> needRequestPermissonList = findDeniedPermissions(activity, permissions);
if (null != needRequestPermissonList
&& needRequestPermissonList.size() > 0) {
ActivityCompat.requestPermissions(activity,
needRequestPermissonList.toArray(
new String[needRequestPermissonList.size()]),
permissRequestCode);
}
}

/**
* 获取权限中需要申请权限的列表
*/
public static List<String> findDeniedPermissions(Activity activity, String[] permissions) {
List<String> needRequestPermissonList = new ArrayList<String>();
for (String perm : permissions) {
if (ContextCompat.checkSelfPermission(activity,
perm) != PackageManager.PERMISSION_GRANTED) {
needRequestPermissonList.add(perm);
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(
activity, perm)) {
needRequestPermissonList.add(perm);
}
}
}
return needRequestPermissonList;
}

public static boolean checkSelfPermission(Context context, String[] permissions) {
for (String perm : permissions) {
if (ContextCompat.checkSelfPermission(context, perm) != PackageManager.PERMISSION_GRANTED) {
return true;
}
}
return false;
}

public static boolean checkSelfResult(int[] grantResults) {
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
}

 
onRequestPermissionsResult回调方法中则这样处理:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);

if (requestCode == 0) {
if (PermissionUtils.checkSelfResult(grantResults)) {
// Permission Granted
//处理业务逻辑
} else {
// Permission Denied

if (null == mDialog)
mDialog = new AlertDialog.Builder(SplashActivity.this)
.setTitle("友好提醒")
.setMessage("没有权限将不能更好的使用,请开启权限!")
.setPositiveButton("开启", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
ShowAppSetDetails.showInstalledAppDetails(SplashActivity.this, "user.zhuku.com");
LogPrint.logILsj(TAG, "开启权限设置");
}
})
.setCancelable(false)
.create();

if (!mDialog.isShowing()) {
mDialog.show();
}
}
}
}

 
如果项目需要在页面可见时进行权限申请,请放在onStart()方法中,不要写在onResume()中。
可以想象一下,如果写在onResume()中,当用户同意了权限,则无碍,若是点击拒绝,则会回调权限拒绝的方法,这时系统申请权限的对话框消失不见,会再次调用onResume()请求权限显示对话框,若是还拒绝,则会再次调用onResume()方法,一直处于死循环。
因为系统请求权限的对话框其实一个开启了一个Activity。部分源码如下:  
 
 public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
if (mHasCurrentPermissionsRequest) {
Log.w(TAG, "Can reqeust only one set of permissions at a time");
// Dispatch the callback with empty arrays which means a cancellation.
onRequestPermissionsResult(requestCode, new String[0], new int[0]);
return;
}
Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null);
mHasCurrentPermissionsRequest = true;
}

至此,老司机本次发车已到终点,这一程体验,你还不会封装自己的运行时权限库吗?
 
 

qrcode_for_gh_08bfa7313fb2_258.jpg

微信公众号:IT大前端
关注可了解更多的大前端领域技术 收起阅读 »

2018,环信是如何C位出道的!(分享你和环信的故事赢千元奖励)

六年,筚路蓝缕,环信走过了一段从无至有的征程;

六年,栉风沐雨,见证了中国SaaS从0到1到1++的幸运;

六年,砥砺前行,从IM云1.0到IM云4.0,从移动客服到全媒体客服再到智能客服;

六年,峥嵘岁月,又一个全新的起点等待环信人去超越,从“心”出发;

六载春秋,陪伴是最长情的告白!感恩有你!!!


新年广告_01.jpg


新年广告_02.jpg


新年广告_03.jpg


新年广告_05.jpg


新年广告_06.jpg


新年广告_07.jpg


新年广告_08.jpg


新年广告_09.jpg


新年广告_10.jpg


新年广告_11.jpg

分享你和环信的故事赢千元奖励

欢迎在评论区分享你和环信的故事,评论区点赞前3名各送200元京东卡一张,第4-10名各送100元京东卡一张。(春节后第一个工作日2月11日公布获奖名单)

评论地址:https://mp.weixin.qq.com/s/Tij4kpquSUSeB04lkcepXQ
继续阅读 »
六年,筚路蓝缕,环信走过了一段从无至有的征程;

六年,栉风沐雨,见证了中国SaaS从0到1到1++的幸运;

六年,砥砺前行,从IM云1.0到IM云4.0,从移动客服到全媒体客服再到智能客服;

六年,峥嵘岁月,又一个全新的起点等待环信人去超越,从“心”出发;

六载春秋,陪伴是最长情的告白!感恩有你!!!


新年广告_01.jpg


新年广告_02.jpg


新年广告_03.jpg


新年广告_05.jpg


新年广告_06.jpg


新年广告_07.jpg


新年广告_08.jpg


新年广告_09.jpg


新年广告_10.jpg


新年广告_11.jpg

分享你和环信的故事赢千元奖励

欢迎在评论区分享你和环信的故事,评论区点赞前3名各送200元京东卡一张,第4-10名各送100元京东卡一张。(春节后第一个工作日2月11日公布获奖名单)

评论地址:https://mp.weixin.qq.com/s/Tij4kpquSUSeB04lkcepXQ 收起阅读 »

最近开发的即时通讯软件,有ios和安卓和web端


hongbao4.png


iphone-slide-1.png


iphone-slide-2.png


iphone-slide-4.png


iphone-slide-6.png


iphone-slide-7.png


iphone-slide-8.png

支持群聊,单聊,发送语音,红包等功能
以及强大的群组功能
https://fir.im/Aichat
有不足之处还请多多指教,欢迎加我qq咨询,244451417
继续阅读 »

hongbao4.png


iphone-slide-1.png


iphone-slide-2.png


iphone-slide-4.png


iphone-slide-6.png


iphone-slide-7.png


iphone-slide-8.png

支持群聊,单聊,发送语音,红包等功能
以及强大的群组功能
https://fir.im/Aichat
有不足之处还请多多指教,欢迎加我qq咨询,244451417 收起阅读 »

在线充值成功后,如何开发票?

如您已经在线支付成功,请您联系400-612-1986或者点击页面 http://www.easemob.com/ 右侧的商务咨询申请发票,仅限增值税普通发票(温馨提示:400和商务咨询申请只支持增值税普通发票,不能申请增值税专用发票,增值税专用发票请联系您的商务经理开通)。在线支付成功后(包含新购、增购、续费),可以在90天内提交申请开具增值税普通发票。自助申请发票统一由财务受理后,将于20个工作日左右寄出,邮寄方式:普通快递-优速快递
继续阅读 »
如您已经在线支付成功,请您联系400-612-1986或者点击页面 http://www.easemob.com/ 右侧的商务咨询申请发票,仅限增值税普通发票(温馨提示:400和商务咨询申请只支持增值税普通发票,不能申请增值税专用发票,增值税专用发票请联系您的商务经理开通)。在线支付成功后(包含新购、增购、续费),可以在90天内提交申请开具增值税普通发票。自助申请发票统一由财务受理后,将于20个工作日左右寄出,邮寄方式:普通快递-优速快递 收起阅读 »

h5仿微信语音聊天|仿微信摇一摇|地图定位|数字支付键盘

基于html5+css3+wcPop+swiper+zepto+weScroll等技术实现的高仿微信界面|仿微信语音聊天效果|仿微信摇一摇加好友|仿微信支付键盘。摇一摇模块则是使用了shake.js插件实现功能效果的,语音模块效果和微信语音效果非常类似,按住说话,手指上滑、取消发送。
https://blog.csdn.net/yanxinyun1990/article/details/85221037

022360截图20181221095816291.png


023360截图20181221095950859.png


025360截图20181221100205642.png


026360截图20181221100230225.png


002360截图20181221094619690.png


003360截图20181221094702001.png


004360截图20181221094720201.png


007360截图20181221095016435.png


009360截图20181221095104377.png


010360截图20181221095121562.png


011360截图20181221095141122.png


014360截图20181221095243962.png


016360截图20181221095501306.png


017360截图20181221095514866.png


020360截图20181221095719913.png


021360截图20181221095816291.png


027360截图20181221095719913.png


028360截图20181221100336473.png

 
// >>> 【摇一摇加好友核心模块】------------------------------------------
// 摇一摇加好友弹窗
$("#J__popScreen_shake").on("click", function () {
var shakePopIdx = wcPop({
id: 'wcim_shake_fullscreen',
skin: 'fullscreen',
title: '摇一摇',
content: $("#J__popupTmpl-shakeFriends").html(),
position: 'right',
xclose: true,
style: 'background: #303030;',
show: function(){
// 摇一摇功能
var _shake = new Shake({threshold: 15});
_shake.start();
window.addEventListener("shake", function(){
window.navigator.vibrate && navigator.vibrate(500);
// console.log("触发摇一摇!");

$(".J__shakeInfoBox").html("");
$(".J__shakeLoading").fadeIn(300);
// 消息模板
var shakeTpl = [
'<div class="shake-info flexbox flex-alignc">\
<img class="uimg" src="img/uimg/u__chat-img08.jpg" />\
<div class="flex1">\
<h2 class="name">大幂幂<i class="iconfont icon-nv c-f37e7d"></i></h2>\
<label class="lbl clamp1">开森每一刻,每天都要美美哒!</label>\
</div>\
</div>'
].join("");
setTimeout(function(){
$(".J__shakeLoading").fadeOut(300);
$(".J__shakeInfoBox").html(shakeTpl);
}, 1500);
}, false);
}
});
});
// 切换摇一摇项目
$("body").on("click", ".J__swtShakeItem a", function(){
$(this).addClass("active").siblings().removeClass("active");
});
// 摇一摇设置
$("body").on("click", ".J__shakeSetting", function(){
wcPop({
skin: 'actionsheetMini',
anim: 'footer',
btns: [
{ text: '<div class="flexbox flex-alignc"><span class="flex1">是否开启震动</span> <span class="rpr-30"><input class="cp__checkboxPX-switch" type="checkbox" checked /></span></div>' },
{ text: '摇到的历史' },
]
});
});
// ripple波纹效果
wcRipple({ elem: '.effect__ripple-fff', opacity: .15, speed: .5, bgColor: "#fff" });
wcRipple({ elem: '.effect__ripple', opacity: .15, speed: .5, bgColor: "#000" });

// 禁止长按弹出系统菜单
$("body").on("contextmenu", ".weChatIM__panel", function (e) {
e.preventDefault();
});

// 顶部 “+” 菜单
$("#J__topbarAdd").on("click", function(e){
var _points = [e.clientX, e.clientY];
var contextMenuIdx = wcPop({
skin: 'contextmenu', shade: true,shadeClose: true,opacity: 0,follow: _points,
style: 'background:#3d3f4e; min-width:3.5rem;',
btns: [
{text: '<i class="iconfont icon-haoyou fs-40 mr-10"></i><span>添加好友</span>',style: 'color:#fff;', onTap(){
wcPop.close(contextMenuIdx);
// 添加好友
var addfriendIdx = wcPop({
id: 'wcim_fullscreen',
skin: 'fullscreen',
title: '添加好友',
content: $("#J__popupTmpl-addFriends").html(),
position: 'right',
opacity: .1,
xclose: true,
style: 'background: #f2f1f6;'
});
}},
{text: '<i class="iconfont icon-qunliao fs-40 mr-10"></i><span>发起群聊</span>',style: 'color:#fff;', onTap(){
wcPop.close(contextMenuIdx);
// 发起群聊
var addfriendIdx = wcPop({
id: 'wcim_fullscreen',
skin: 'fullscreen',
title: '发起群聊',
content: $("#J__popupTmpl-launchGroupChat").html(),
position: 'right',
opacity: .1,
xclose: true,
style: 'background: #f2f1f6;'
});
}},
{text: '<i class="iconfont icon-bangzhu fs-40 mr-10"></i><span>帮助与反馈</span>',style: 'color:#fff;',}
]
});
});
// ...获取语音时长
getVoiceTime();
function getVoiceTime(){
$("#J__chatMsgList li .audio").each(function () {
var that = $(this), audio = that.find("audio")[0], duration;
audio.load();
audio.oncanplay = function(){
duration = Math.ceil(audio.duration);
if (duration == 'Infinity') {
getVoiceTime();
} else {
that.find(".time").text(duration + `''`);
that.attr("data-time", duration);
// 语音宽度%
var percent = (duration / 60).toFixed(2) * 100 + 20 + '%';
that.css("width", percent);
}
}
});
}

20180817002157557.jpg

欢迎大家一起交流、学习  Q:282310962  wx:xy190310
 
继续阅读 »
基于html5+css3+wcPop+swiper+zepto+weScroll等技术实现的高仿微信界面|仿微信语音聊天效果|仿微信摇一摇加好友|仿微信支付键盘。摇一摇模块则是使用了shake.js插件实现功能效果的,语音模块效果和微信语音效果非常类似,按住说话,手指上滑、取消发送。
https://blog.csdn.net/yanxinyun1990/article/details/85221037

022360截图20181221095816291.png


023360截图20181221095950859.png


025360截图20181221100205642.png


026360截图20181221100230225.png


002360截图20181221094619690.png


003360截图20181221094702001.png


004360截图20181221094720201.png


007360截图20181221095016435.png


009360截图20181221095104377.png


010360截图20181221095121562.png


011360截图20181221095141122.png


014360截图20181221095243962.png


016360截图20181221095501306.png


017360截图20181221095514866.png


020360截图20181221095719913.png


021360截图20181221095816291.png


027360截图20181221095719913.png


028360截图20181221100336473.png

 
// >>> 【摇一摇加好友核心模块】------------------------------------------
// 摇一摇加好友弹窗
$("#J__popScreen_shake").on("click", function () {
var shakePopIdx = wcPop({
id: 'wcim_shake_fullscreen',
skin: 'fullscreen',
title: '摇一摇',
content: $("#J__popupTmpl-shakeFriends").html(),
position: 'right',
xclose: true,
style: 'background: #303030;',
show: function(){
// 摇一摇功能
var _shake = new Shake({threshold: 15});
_shake.start();
window.addEventListener("shake", function(){
window.navigator.vibrate && navigator.vibrate(500);
// console.log("触发摇一摇!");

$(".J__shakeInfoBox").html("");
$(".J__shakeLoading").fadeIn(300);
// 消息模板
var shakeTpl = [
'<div class="shake-info flexbox flex-alignc">\
<img class="uimg" src="img/uimg/u__chat-img08.jpg" />\
<div class="flex1">\
<h2 class="name">大幂幂<i class="iconfont icon-nv c-f37e7d"></i></h2>\
<label class="lbl clamp1">开森每一刻,每天都要美美哒!</label>\
</div>\
</div>'
].join("");
setTimeout(function(){
$(".J__shakeLoading").fadeOut(300);
$(".J__shakeInfoBox").html(shakeTpl);
}, 1500);
}, false);
}
});
});
// 切换摇一摇项目
$("body").on("click", ".J__swtShakeItem a", function(){
$(this).addClass("active").siblings().removeClass("active");
});
// 摇一摇设置
$("body").on("click", ".J__shakeSetting", function(){
wcPop({
skin: 'actionsheetMini',
anim: 'footer',
btns: [
{ text: '<div class="flexbox flex-alignc"><span class="flex1">是否开启震动</span> <span class="rpr-30"><input class="cp__checkboxPX-switch" type="checkbox" checked /></span></div>' },
{ text: '摇到的历史' },
]
});
});
// ripple波纹效果
wcRipple({ elem: '.effect__ripple-fff', opacity: .15, speed: .5, bgColor: "#fff" });
wcRipple({ elem: '.effect__ripple', opacity: .15, speed: .5, bgColor: "#000" });

// 禁止长按弹出系统菜单
$("body").on("contextmenu", ".weChatIM__panel", function (e) {
e.preventDefault();
});

// 顶部 “+” 菜单
$("#J__topbarAdd").on("click", function(e){
var _points = [e.clientX, e.clientY];
var contextMenuIdx = wcPop({
skin: 'contextmenu', shade: true,shadeClose: true,opacity: 0,follow: _points,
style: 'background:#3d3f4e; min-width:3.5rem;',
btns: [
{text: '<i class="iconfont icon-haoyou fs-40 mr-10"></i><span>添加好友</span>',style: 'color:#fff;', onTap(){
wcPop.close(contextMenuIdx);
// 添加好友
var addfriendIdx = wcPop({
id: 'wcim_fullscreen',
skin: 'fullscreen',
title: '添加好友',
content: $("#J__popupTmpl-addFriends").html(),
position: 'right',
opacity: .1,
xclose: true,
style: 'background: #f2f1f6;'
});
}},
{text: '<i class="iconfont icon-qunliao fs-40 mr-10"></i><span>发起群聊</span>',style: 'color:#fff;', onTap(){
wcPop.close(contextMenuIdx);
// 发起群聊
var addfriendIdx = wcPop({
id: 'wcim_fullscreen',
skin: 'fullscreen',
title: '发起群聊',
content: $("#J__popupTmpl-launchGroupChat").html(),
position: 'right',
opacity: .1,
xclose: true,
style: 'background: #f2f1f6;'
});
}},
{text: '<i class="iconfont icon-bangzhu fs-40 mr-10"></i><span>帮助与反馈</span>',style: 'color:#fff;',}
]
});
});
// ...获取语音时长
getVoiceTime();
function getVoiceTime(){
$("#J__chatMsgList li .audio").each(function () {
var that = $(this), audio = that.find("audio")[0], duration;
audio.load();
audio.oncanplay = function(){
duration = Math.ceil(audio.duration);
if (duration == 'Infinity') {
getVoiceTime();
} else {
that.find(".time").text(duration + `''`);
that.attr("data-time", duration);
// 语音宽度%
var percent = (duration / 60).toFixed(2) * 100 + 20 + '%';
that.css("width", percent);
}
}
});
}

20180817002157557.jpg

欢迎大家一起交流、学习  Q:282310962  wx:xy190310
  收起阅读 »

钉钉h5版|仿钉钉办公|仿钉钉聊天界面

h5仿钉钉移动办公|仿钉钉聊天界面|钉钉html5模板


基于钉钉的H5开发,实现的仿钉钉移动智慧办公系统weDingTalk,使用html5+css3+zepto+jquery+swiper+wcPop等技术进行搭配开发,可以发送消息、表情,图片、视频预览,打赏、发红包,长按消息操作菜单……

001360截图20180915003708059.png


004360截图20180915005419923.png


005360截图20180915005728692.png


006360截图20180915005901996.png


008360截图20180915010335077.png


010360截图20180915013242999.png


011360截图20180915013303062.png


012360截图20180915010948002.png


013360截图20180915011004813.png


014360截图20180915011140916.png


015360截图20180915011705229.png


017360截图20180915011927963.png


019360截图20180915012132931.png

 
整体布局采用750px+fontSize.js适配设备:
(function(doc, win) {
var docEl = doc.documentElement,
resizeEvt = "orientationchange" in window ? "orientationchange" : "resize",
recalc = function() {
var clientWidth = docEl.clientWidth;
if (!clientWidth) return;
if (clientWidth >= 750) {
docEl.style.fontSize = "100px";
} else {
docEl.style.fontSize = 100 * (clientWidth / 750) + "px";
}
};

if (!doc.addEventListener) return;
recalc();
win.addEventListener(resizeEvt, recalc, false);
})(document, window);
// ...打赏朋友功能
$(".J__chooseDs").on("click", function () {
var dashangIdx = wcPop({
id: 'wdtPopHb',
title: '<a class="popui__tit-back wdt__ripple" href="javascript:;" onclick="wcPop.close();"><i class="iconfont icon-back"></i></a> <div class="popui__tit-text">打赏</div>',
skin: 'fullscreen',
content: $("#J__popupTmpl-Dashang").html(),
position: 'right',
style: 'background: #f3f3f3;',

show: function(){
$("body").on("click", "#J__chooseGift .gift", function () {
$(this).addClass("selected").siblings().removeClass("selected");
});
}
});
});

// ...拆开红包功能
$(".J__getRedPackets").on("click", function(){
var getHbIdx = wcPop({
id: 'wdtPopGetHb',
skin: 'ios',
content: $("#J__popupTmpl-getRedPacket").html(),
xclose: true,
style: 'background-color: #f3f3f3; width: 280px;',

show: function () {
$("body").on("click", ".J__btnGetRedPacket", function () {
var that = $(this);
that.addClass("active");
setTimeout(function(){
that.removeClass("active");
}, 1000);
});
}
});
});

// ...查看红包功能
$(".J__viewRedPackets").on("click", function(){
var viewHbIdx = wcPop({
id: 'wdtPopHb',
title: '<a class="popui__tit-back wdt__ripple" href="javascript:;" onclick="wcPop.close();"><i class="iconfont icon-back"></i></a> <div class="popui__tit-text">红包详情</div>',
skin: 'fullscreen',
content: $("#J__popupTmpl-viewRedPacket").html(),
position: 'top',
style: 'background: #f3f3f3;'
});
});

  • 欢迎大家一起交流、学习  Q:282310962  wx:xy190310

继续阅读 »

h5仿钉钉移动办公|仿钉钉聊天界面|钉钉html5模板


基于钉钉的H5开发,实现的仿钉钉移动智慧办公系统weDingTalk,使用html5+css3+zepto+jquery+swiper+wcPop等技术进行搭配开发,可以发送消息、表情,图片、视频预览,打赏、发红包,长按消息操作菜单……

001360截图20180915003708059.png


004360截图20180915005419923.png


005360截图20180915005728692.png


006360截图20180915005901996.png


008360截图20180915010335077.png


010360截图20180915013242999.png


011360截图20180915013303062.png


012360截图20180915010948002.png


013360截图20180915011004813.png


014360截图20180915011140916.png


015360截图20180915011705229.png


017360截图20180915011927963.png


019360截图20180915012132931.png

 
整体布局采用750px+fontSize.js适配设备:
(function(doc, win) {
var docEl = doc.documentElement,
resizeEvt = "orientationchange" in window ? "orientationchange" : "resize",
recalc = function() {
var clientWidth = docEl.clientWidth;
if (!clientWidth) return;
if (clientWidth >= 750) {
docEl.style.fontSize = "100px";
} else {
docEl.style.fontSize = 100 * (clientWidth / 750) + "px";
}
};

if (!doc.addEventListener) return;
recalc();
win.addEventListener(resizeEvt, recalc, false);
})(document, window);
// ...打赏朋友功能
$(".J__chooseDs").on("click", function () {
var dashangIdx = wcPop({
id: 'wdtPopHb',
title: '<a class="popui__tit-back wdt__ripple" href="javascript:;" onclick="wcPop.close();"><i class="iconfont icon-back"></i></a> <div class="popui__tit-text">打赏</div>',
skin: 'fullscreen',
content: $("#J__popupTmpl-Dashang").html(),
position: 'right',
style: 'background: #f3f3f3;',

show: function(){
$("body").on("click", "#J__chooseGift .gift", function () {
$(this).addClass("selected").siblings().removeClass("selected");
});
}
});
});

// ...拆开红包功能
$(".J__getRedPackets").on("click", function(){
var getHbIdx = wcPop({
id: 'wdtPopGetHb',
skin: 'ios',
content: $("#J__popupTmpl-getRedPacket").html(),
xclose: true,
style: 'background-color: #f3f3f3; width: 280px;',

show: function () {
$("body").on("click", ".J__btnGetRedPacket", function () {
var that = $(this);
that.addClass("active");
setTimeout(function(){
that.removeClass("active");
}, 1000);
});
}
});
});

// ...查看红包功能
$(".J__viewRedPackets").on("click", function(){
var viewHbIdx = wcPop({
id: 'wdtPopHb',
title: '<a class="popui__tit-back wdt__ripple" href="javascript:;" onclick="wcPop.close();"><i class="iconfont icon-back"></i></a> <div class="popui__tit-text">红包详情</div>',
skin: 'fullscreen',
content: $("#J__popupTmpl-viewRedPacket").html(),
position: 'top',
style: 'background: #f3f3f3;'
});
});

  • 欢迎大家一起交流、学习  Q:282310962  wx:xy190310

收起阅读 »

微信小程序弹窗插件wcPop实例开发|自定义弹窗组件

微信小程序自定义showModel模态弹窗|alert警告弹窗|toast自定义图标弹窗
微信小程序自定义弹窗插件实战——wcPop组件,之前就想写一个小程序自定义弹窗插件扩展,但是由于时间的关系,一直真正进行开发。好吧,反正最近这段时间稍微比较清闲,趁着这个机会,就学习了下微信插件开发。
https://blog.csdn.net/yanxinyun1990

360截图20181208133703698.png


001360截图20181117110044432.png


002360截图20181117110217296.png


003360截图20181117110249704.png


004360截图20181117110334240.png


005360截图20181117110613448.png


006360截图20181117110931312.png


007360截图20181117111007440.png


008360截图20181117111809104.png


011360截图20181117111916481.png


012360截图20181117112123619.png


014360截图20181117112238337.png


016360截图20181117112359769.png


017360截图20181117112517601.png


018360截图20181117112734640.png

 
继续阅读 »
微信小程序自定义showModel模态弹窗|alert警告弹窗|toast自定义图标弹窗
微信小程序自定义弹窗插件实战——wcPop组件,之前就想写一个小程序自定义弹窗插件扩展,但是由于时间的关系,一直真正进行开发。好吧,反正最近这段时间稍微比较清闲,趁着这个机会,就学习了下微信插件开发。
https://blog.csdn.net/yanxinyun1990

360截图20181208133703698.png


001360截图20181117110044432.png


002360截图20181117110217296.png


003360截图20181117110249704.png


004360截图20181117110334240.png


005360截图20181117110613448.png


006360截图20181117110931312.png


007360截图20181117111007440.png


008360截图20181117111809104.png


011360截图20181117111916481.png


012360截图20181117112123619.png


014360截图20181117112238337.png


016360截图20181117112359769.png


017360截图20181117112517601.png


018360截图20181117112734640.png

  收起阅读 »

h5直播webapp项目|html5直播案例|h5直播模板

html5微直播聊天项目实例|weLiveShow直播h5|移动端h5直播|html5直播模板


最近试着利用html5技术开发了下直播项目,使用到了h5+css3+iscroll+zepot+swiper+wlsPop等技术混合模式开发,实现了群友互动聊天、新消息提醒、礼物动效展示,打赏主播、发红包、送礼物……
https://blog.csdn.net/yanxinyun1990

001-360截图20181104094234436.png


003-360截图20181104094234436.png


004-360截图20181104095737443.png


005-360截图20181104095812016.png


006-360截图20181104100100390.png


007-360截图20181104100415889.png


009-360截图20181104100729956.png


009-360截图20181104100729956.png


011-360截图20181104100933641.png


013-360截图20181104102314202.png


014-360截图20181104102337969.png


015-360截图20181104102515553.png


015-360截图20181104102733534.png


017-360截图20181104102933051.png


018-360截图20181104103147258.png


019-360截图20181104103243870.png


020-360截图20181104103341497.png


20180817002157557.jpg

 
 
 
继续阅读 »

html5微直播聊天项目实例|weLiveShow直播h5|移动端h5直播|html5直播模板


最近试着利用html5技术开发了下直播项目,使用到了h5+css3+iscroll+zepot+swiper+wlsPop等技术混合模式开发,实现了群友互动聊天、新消息提醒、礼物动效展示,打赏主播、发红包、送礼物……
https://blog.csdn.net/yanxinyun1990

001-360截图20181104094234436.png


003-360截图20181104094234436.png


004-360截图20181104095737443.png


005-360截图20181104095812016.png


006-360截图20181104100100390.png


007-360截图20181104100415889.png


009-360截图20181104100729956.png


009-360截图20181104100729956.png


011-360截图20181104100933641.png


013-360截图20181104102314202.png


014-360截图20181104102337969.png


015-360截图20181104102515553.png


015-360截图20181104102733534.png


017-360截图20181104102933051.png


018-360截图20181104103147258.png


019-360截图20181104103243870.png


020-360截图20181104103341497.png


20180817002157557.jpg

 
 
  收起阅读 »

十六位互联网大咖,齐邀你共赴七麦【NextWorld2018】增长盛宴!

今年的 12 月 12 日,不仅有阿里的“双十二”购物日,还有一场属于你的互联网年末盛宴。

12 月 12 日 9:00 — 17:00。

北京 · 四季酒店 · 五层宴会厅。(朝阳区亮马桥路 48 号)

十六位互联网行业大咖,齐聚【NextWorld2018 新原力增长】峰会,邀你一起深度解锁用户增长新姿势!

微信图片_20181206111909.jpg

最具价值峰会,齐聚大咖风采
 
清科集团联席总裁【袁润兵】,为你解读互联网资本浪潮下的新趋势。
 
数看全球:国内市场流量红利消失,全球市场近况如何?
 
AppsFlyer 大中华地区商务副总裁【徐宇】,带你揭秘全球移动应用市场现状和出海新机会。
 
OneSight CEO【李蕾】,解析出海营销新技术,助力移动产品新增长。
 
UMKA 俄罗斯电商平台中国区副总裁【Sofia Zhang】,分享如何实现用户本地化爆发式增长的宝贵经验。
 
游艺道副总裁【孙可】,评说出海新红利,快速崛起的跨境手游。
 
聚焦增长:产品增长乏力,如何为产品赋能,实现高速增长?
 
樊登读书会副总裁【孙向利】,分享关于知识付费时代,如何激发用户自传播力,助力产品增长。
 
十点读书 CTO【张峡】,解读十点读书如何通过矩阵式布局,一年涨粉上千万。
 
社群裂变:获客成本日益升高,产品如何通过裂变,零成本精准获客?
 
知乎商业市场经理【陈欣】,为你解析内容创意如何引爆产品病毒式增长。

微信图片_20181206112024.jpg

创业家副总裁【易涛】,分享黑马营如何通过社群营销打造市值 50 亿的“创业基地”。
微信图片_20181206112224.jpg

产业互联网:传统行业如何借力互联网实现新增长?
 
连咖啡合伙人【张洪基】,和你聊聊连咖啡的社交营销新模式。
微信图片_20181206112245.jpg

Plum 平台创始人兼 CEO【徐薇】,解析 Plum 如何实现二手时尚平台交易量的几十倍增长。
 
 
拉勾招聘联合创始人&CMO【鲍艾乐】,带来拉勾年度大数据,俯瞰“增长er”的生存地图。
微信图片_20181206112311.jpg

以为这就完事了?错!惊喜还远未结束!
 
新产品!

2018 年最值得期待的 App 出海推广平台全新重磅发布!七麦技术团队历时两年诚意巨献,让你出海有术!
 
加人脉!
 
峰会到场数十位移动互联网意见领袖,1000+ 精英创业、推广从业者到场,覆盖推广、营销、运营多领域从业人员,互联网最牛的人等你来撩~ 资源、人脉无压力!
 
有福利!
 
现场设“2018年度推广增长大礼包”有料有趣高价值!
 
扫描下图二维码,马上报名参加吧!
 
 
和七麦一起,共赴年末增长盛宴!
微信图片_20181206112334.jpg

 
 NextWorld 2018新原力增长峰会了解详情
继续阅读 »
今年的 12 月 12 日,不仅有阿里的“双十二”购物日,还有一场属于你的互联网年末盛宴。

12 月 12 日 9:00 — 17:00。

北京 · 四季酒店 · 五层宴会厅。(朝阳区亮马桥路 48 号)

十六位互联网行业大咖,齐聚【NextWorld2018 新原力增长】峰会,邀你一起深度解锁用户增长新姿势!

微信图片_20181206111909.jpg

最具价值峰会,齐聚大咖风采
 
清科集团联席总裁【袁润兵】,为你解读互联网资本浪潮下的新趋势。
 
数看全球:国内市场流量红利消失,全球市场近况如何?
 
AppsFlyer 大中华地区商务副总裁【徐宇】,带你揭秘全球移动应用市场现状和出海新机会。
 
OneSight CEO【李蕾】,解析出海营销新技术,助力移动产品新增长。
 
UMKA 俄罗斯电商平台中国区副总裁【Sofia Zhang】,分享如何实现用户本地化爆发式增长的宝贵经验。
 
游艺道副总裁【孙可】,评说出海新红利,快速崛起的跨境手游。
 
聚焦增长:产品增长乏力,如何为产品赋能,实现高速增长?
 
樊登读书会副总裁【孙向利】,分享关于知识付费时代,如何激发用户自传播力,助力产品增长。
 
十点读书 CTO【张峡】,解读十点读书如何通过矩阵式布局,一年涨粉上千万。
 
社群裂变:获客成本日益升高,产品如何通过裂变,零成本精准获客?
 
知乎商业市场经理【陈欣】,为你解析内容创意如何引爆产品病毒式增长。

微信图片_20181206112024.jpg

创业家副总裁【易涛】,分享黑马营如何通过社群营销打造市值 50 亿的“创业基地”。
微信图片_20181206112224.jpg

产业互联网:传统行业如何借力互联网实现新增长?
 
连咖啡合伙人【张洪基】,和你聊聊连咖啡的社交营销新模式。
微信图片_20181206112245.jpg

Plum 平台创始人兼 CEO【徐薇】,解析 Plum 如何实现二手时尚平台交易量的几十倍增长。
 
 
拉勾招聘联合创始人&CMO【鲍艾乐】,带来拉勾年度大数据,俯瞰“增长er”的生存地图。
微信图片_20181206112311.jpg

以为这就完事了?错!惊喜还远未结束!
 
新产品!

2018 年最值得期待的 App 出海推广平台全新重磅发布!七麦技术团队历时两年诚意巨献,让你出海有术!
 
加人脉!
 
峰会到场数十位移动互联网意见领袖,1000+ 精英创业、推广从业者到场,覆盖推广、营销、运营多领域从业人员,互联网最牛的人等你来撩~ 资源、人脉无压力!
 
有福利!
 
现场设“2018年度推广增长大礼包”有料有趣高价值!
 
扫描下图二维码,马上报名参加吧!
 
 
和七麦一起,共赴年末增长盛宴!
微信图片_20181206112334.jpg

 
 NextWorld 2018新原力增长峰会了解详情 收起阅读 »

【活动推荐】ECUG Con 2018 拥抱下一个十年

国内云计算领域大咖  许式伟 
倾情发起的技术盛宴
引领国内云领域风向的高端峰会
ECUG Con 2018
2018 年 12 月 22-23 日 深圳
全新启程!
ECUG Con 2018

七牛云 CEO 许式伟

PingCAP CEO 刘奇

七牛云产品副总裁戴文军

Gopher 社区创始人 Asta Xie

阿里巴巴技术专家孙宏亮

《Kubernetes IN ACTION》作者 Marko Lukša

华为云 AI 推理平台&云搜索技术总监胡斐然

七牛云技术总监陈超

阿里云高级开发工程师严明明

京东云区块链实验室与数据库部负责人郭里靖

网易研究院云计算资深架构师朱剑峰

腾讯云高级工程师刘兆瑞

货拉拉数据分析负责人高遥

......
超豪华讲师阵容!

超有料精彩分享!

ECUG 历经十年蜕变

邀您共同开启下个十年

让我们坚持技术情怀,秉承技术精神

开启新的云计算布道篇章!
 
时  间

2018 年 12 月 22-23 日

地  点

深圳市南山区软件产业基地 

更多详情请见下方海报~
30943258454939062.jpg

扫描上方二维码 ,立即购买早鸟票

与大咖讲师共同探索云计算的下一个十年!
活动详情:了解更多
继续阅读 »
国内云计算领域大咖  许式伟 
倾情发起的技术盛宴
引领国内云领域风向的高端峰会
ECUG Con 2018
2018 年 12 月 22-23 日 深圳
全新启程!
ECUG Con 2018

七牛云 CEO 许式伟

PingCAP CEO 刘奇

七牛云产品副总裁戴文军

Gopher 社区创始人 Asta Xie

阿里巴巴技术专家孙宏亮

《Kubernetes IN ACTION》作者 Marko Lukša

华为云 AI 推理平台&云搜索技术总监胡斐然

七牛云技术总监陈超

阿里云高级开发工程师严明明

京东云区块链实验室与数据库部负责人郭里靖

网易研究院云计算资深架构师朱剑峰

腾讯云高级工程师刘兆瑞

货拉拉数据分析负责人高遥

......
超豪华讲师阵容!

超有料精彩分享!

ECUG 历经十年蜕变

邀您共同开启下个十年

让我们坚持技术情怀,秉承技术精神

开启新的云计算布道篇章!
 
时  间

2018 年 12 月 22-23 日

地  点

深圳市南山区软件产业基地 

更多详情请见下方海报~
30943258454939062.jpg

扫描上方二维码 ,立即购买早鸟票

与大咖讲师共同探索云计算的下一个十年!
活动详情:了解更多 收起阅读 »

IT部门信息化正确打开方式



面对大数据时代企业纷纷开始了信息化建设,成立IT部门。对一家企业来说信息是至关重要,如果你问老板每天最想看到得是什么,那一定是数据信息。

一个公司其核心是业务平台,如何提高公司的营业额就要提高业务平台的管理模式,资源不清晰、信息孤岛等这都是旧模式的弊端,信息化的目的是要使企业充分开发和有效利用信息资源,把握机会,做出正确决策,增进企业运行效率,最终提高企业的竞争力水平。其重要性体现在以下方面:

1、有效地降低企业成本

信息技术应用范围涉及整个企业的经济活动,可以有效地、大幅度地降低企业的费用。

2、促进组织结构优化,提高快速反应能力

而在信息技术的支持下,企业可以简化企业组织生产经营的方式,减少中间环节和中间管理人员,从而建立起精良、敏捷、具有创新精神的“扁平”型组织结构。

3、加快产品和技术的创新

现代信息技术与制造的结合所形成的各种企业信息技术,如计算机辅助设计、计算机辅助制造、计算机辅助工艺编制、柔性制造系统、敏捷制造、计算机集成制造系统等,实现了企业开发、设计、制造、营销及管理的高度集成化,极大地增强了企业生产的柔性、敏捷性和适应性。

4、提高企业的市场把握能力

信息技术的应用缩短了企业与消费者的距离,企业与供应商及客户建立起高效、快速的联系,从而提高了企业把握市场和消费者的能力,使企业能迅速根据消费者的需求变化有针对地进行研究与开发话动,及时改变和调整经营战略,不断向市场提供质量更好、品种更多、更适合消费者需求的产品和服务;

5、促进企业提高管理水平

企业信息化不只是计算机硬件本身,更为重要的是与管理的有机结合。即在信息化过程中引进的不仅是信息技术,而更多的是通过转变传统的管理观念,把先进的管理理念、企业管理制度和方法引入到管理流程中,进行管理创新。

6、提升企业人力资源素质

企业信息化,可以加速知识在企业中的传播,使企业领导至全体员工知识水平、信息意识与信息利用能力提高,提升了企业人力资源的素质及企业文化的环境。

7、提高企业决策的科学性、正确性

信息技术改变了企业获取信息、收集信息和传递信息的方式,使管理者对企业内部和外部信息的掌握更加完备、及时和准确。

面对企业信息化如此多的优势,许多IT部门却对信息化一筹莫展。信息化建设需要的是技术和流程,流程是不缺的而IT部门是企业一个边缘化的部门,不可能像软件公司那样有完整的团队,开发难度非常大。

还有就是借助快速开发平台,百度理解是:以某种编程语言或者某几种编程语言为基础,开发出来的一个软件,而这软件不是一个最终的软件产品,它是一个二次开发软件框架,用户可以在这个产品上进行各种各样的软件产品的开发,并且在这个产品上进行开发的时候,不需要像以往的编程方式那样编写大量的代码,而是只需要进行一些简单的配置,或者是写极少量的代码便可以完成一个业务系统的开发工作。比如力软快速开发平台的框架是被反复使用的,有着较高的安全性,它又是一套源码可以直接进行二次开发,所开发的系统有自主产权。

快速开发平台的性质非常适合IT部门,快速开发平台提供的基础框架技术恰恰是IT部门所欠缺的,一般框架已经实现大部分技术要求,而IT部门所要做的就是实现业务要求了。
继续阅读 »


面对大数据时代企业纷纷开始了信息化建设,成立IT部门。对一家企业来说信息是至关重要,如果你问老板每天最想看到得是什么,那一定是数据信息。

一个公司其核心是业务平台,如何提高公司的营业额就要提高业务平台的管理模式,资源不清晰、信息孤岛等这都是旧模式的弊端,信息化的目的是要使企业充分开发和有效利用信息资源,把握机会,做出正确决策,增进企业运行效率,最终提高企业的竞争力水平。其重要性体现在以下方面:

1、有效地降低企业成本

信息技术应用范围涉及整个企业的经济活动,可以有效地、大幅度地降低企业的费用。

2、促进组织结构优化,提高快速反应能力

而在信息技术的支持下,企业可以简化企业组织生产经营的方式,减少中间环节和中间管理人员,从而建立起精良、敏捷、具有创新精神的“扁平”型组织结构。

3、加快产品和技术的创新

现代信息技术与制造的结合所形成的各种企业信息技术,如计算机辅助设计、计算机辅助制造、计算机辅助工艺编制、柔性制造系统、敏捷制造、计算机集成制造系统等,实现了企业开发、设计、制造、营销及管理的高度集成化,极大地增强了企业生产的柔性、敏捷性和适应性。

4、提高企业的市场把握能力

信息技术的应用缩短了企业与消费者的距离,企业与供应商及客户建立起高效、快速的联系,从而提高了企业把握市场和消费者的能力,使企业能迅速根据消费者的需求变化有针对地进行研究与开发话动,及时改变和调整经营战略,不断向市场提供质量更好、品种更多、更适合消费者需求的产品和服务;

5、促进企业提高管理水平

企业信息化不只是计算机硬件本身,更为重要的是与管理的有机结合。即在信息化过程中引进的不仅是信息技术,而更多的是通过转变传统的管理观念,把先进的管理理念、企业管理制度和方法引入到管理流程中,进行管理创新。

6、提升企业人力资源素质

企业信息化,可以加速知识在企业中的传播,使企业领导至全体员工知识水平、信息意识与信息利用能力提高,提升了企业人力资源的素质及企业文化的环境。

7、提高企业决策的科学性、正确性

信息技术改变了企业获取信息、收集信息和传递信息的方式,使管理者对企业内部和外部信息的掌握更加完备、及时和准确。

面对企业信息化如此多的优势,许多IT部门却对信息化一筹莫展。信息化建设需要的是技术和流程,流程是不缺的而IT部门是企业一个边缘化的部门,不可能像软件公司那样有完整的团队,开发难度非常大。

还有就是借助快速开发平台,百度理解是:以某种编程语言或者某几种编程语言为基础,开发出来的一个软件,而这软件不是一个最终的软件产品,它是一个二次开发软件框架,用户可以在这个产品上进行各种各样的软件产品的开发,并且在这个产品上进行开发的时候,不需要像以往的编程方式那样编写大量的代码,而是只需要进行一些简单的配置,或者是写极少量的代码便可以完成一个业务系统的开发工作。比如力软快速开发平台的框架是被反复使用的,有着较高的安全性,它又是一套源码可以直接进行二次开发,所开发的系统有自主产权。

快速开发平台的性质非常适合IT部门,快速开发平台提供的基础框架技术恰恰是IT部门所欠缺的,一般框架已经实现大部分技术要求,而IT部门所要做的就是实现业务要求了。 收起阅读 »

0 元福利手慢无 | 年尾盛会——2018 · 支付学院年会环信用户免费报名

文能提笔抒骚情
武能切图画交互


微信图片_20181127145045.jpg

 作为一个产品经理

学习不是忙里偷闲的充电

而是写进岗位职责里的日日功

公司一步步发展,业务一步步扩张
 
 支付 

一个熟悉又陌生的词映入眼帘
从 2014 年起,Ping++ 用 4 年的沉淀积累,化繁为简,实现「 7 行代码接入支付」的聚合支付、会员账户系统、多级商户系统等产品,首次提出「聚合支付」,在聚合支付领域树立标杆、建立规则。曾经踩过的无数坑,都成为了支付这一领域内宝贵的经验。所以说,不如为所有的产品经理们办一个「支付学院」吧!就这样,支付学院第一期款款而来。

微信图片_20181127151035.jpg

「你们应该继续办下去啊!支持你们!」
「这次真的收获满满,老师讲得我还要再捋捋,太多了一下吸收不了。」
「可以私底下给秋秋老师打 Call 吗?请替我表白!」
「PPT 我打印出来学习,你们下次别用黑底白字了,打印起来反白很麻烦。」
「……」
微信图片_20181127151054.jpg

受宠若惊,十分惶恐。因此,领域即便如此狭窄,架构即便大同小异,也要像宰相的女儿山鲁佐德一样,奉上个个迥异的故事,直到第一千零一夜。

微信图片_20181127151125.jpg


微信图片_20181127151137.jpg


微信图片_20181127151148.jpg

于是就又举办了第二期、第三期,以及现在如约而至的 2018 年会。
微信图片_20181127151202.jpg

❤ 作为一名严肃活泼认真可爱的支付从业者,你一定知道支付的前提是合规,监管即是这开头的 1 ;而后作为一个产品商业化的核心,支付系统与交易系统应为业务敞开怀抱,随着需求的变化而迭代以支撑业务十倍百倍的增长,0 是谦虚也是包容;在当今经济下行的处境下,很多企业为了寻求进一步的发展,纷纷选择了出海,去海外寻求新的增长点,因此跨境就像尾部的 1,代表了新的起点和开始。

❤ 1001 个支付故事,其实也是 4 个支付故事。2018 支付学院年会来为大家讲解关于交易系统、支付系统、监管和跨境支付的故事。
 
年会不同以往的是,大家热热闹闹开开心心相聚才是最重要的,因此这次人数应该远超以往,如果想要 0 元听老师讲课,那你就需要手快一些了。

0 元票兑换码
feqylt1

 
有道是,相逢即是有缘,有缘自会相逢。

我觉得我们已经很有缘了,故事也有了,来年会一起聊聊吧。
 
活动详情:免费报名
继续阅读 »
文能提笔抒骚情
武能切图画交互


微信图片_20181127145045.jpg

 作为一个产品经理

学习不是忙里偷闲的充电

而是写进岗位职责里的日日功

公司一步步发展,业务一步步扩张
 
 支付 

一个熟悉又陌生的词映入眼帘
从 2014 年起,Ping++ 用 4 年的沉淀积累,化繁为简,实现「 7 行代码接入支付」的聚合支付、会员账户系统、多级商户系统等产品,首次提出「聚合支付」,在聚合支付领域树立标杆、建立规则。曾经踩过的无数坑,都成为了支付这一领域内宝贵的经验。所以说,不如为所有的产品经理们办一个「支付学院」吧!就这样,支付学院第一期款款而来。

微信图片_20181127151035.jpg

「你们应该继续办下去啊!支持你们!」
「这次真的收获满满,老师讲得我还要再捋捋,太多了一下吸收不了。」
「可以私底下给秋秋老师打 Call 吗?请替我表白!」
「PPT 我打印出来学习,你们下次别用黑底白字了,打印起来反白很麻烦。」
「……」
微信图片_20181127151054.jpg

受宠若惊,十分惶恐。因此,领域即便如此狭窄,架构即便大同小异,也要像宰相的女儿山鲁佐德一样,奉上个个迥异的故事,直到第一千零一夜。

微信图片_20181127151125.jpg


微信图片_20181127151137.jpg


微信图片_20181127151148.jpg

于是就又举办了第二期、第三期,以及现在如约而至的 2018 年会。
微信图片_20181127151202.jpg

❤ 作为一名严肃活泼认真可爱的支付从业者,你一定知道支付的前提是合规,监管即是这开头的 1 ;而后作为一个产品商业化的核心,支付系统与交易系统应为业务敞开怀抱,随着需求的变化而迭代以支撑业务十倍百倍的增长,0 是谦虚也是包容;在当今经济下行的处境下,很多企业为了寻求进一步的发展,纷纷选择了出海,去海外寻求新的增长点,因此跨境就像尾部的 1,代表了新的起点和开始。

❤ 1001 个支付故事,其实也是 4 个支付故事。2018 支付学院年会来为大家讲解关于交易系统、支付系统、监管和跨境支付的故事。
 
年会不同以往的是,大家热热闹闹开开心心相聚才是最重要的,因此这次人数应该远超以往,如果想要 0 元听老师讲课,那你就需要手快一些了。

0 元票兑换码
feqylt1

 
有道是,相逢即是有缘,有缘自会相逢。

我觉得我们已经很有缘了,故事也有了,来年会一起聊聊吧。
 
活动详情:免费报名 收起阅读 »

关乎企业成败:软件应用系统选择要慎重


进入互联网时代以来,企业的信息化建设也逐步进入了繁荣期,但是面对市场上纷繁多样的应用软件,选择便显得很重要了。

在企业市场的争夺中,软件开发的商场竞争也在不断升级,无数的软件公司倒下,又有更多年轻的公司崛起。

面对市场上参差不齐的开发公司,选择时必须要小心谨慎,因为如果不慎选择了不规范的外包公司或者技术能力有限的公司,不仅会让资金打水漂,更有可能打乱产品上线规划,造成公司的严重损失,所以说软件开发必须要筛选正规的软件公司,避免后期的使用风险。

那么具体来说风险会有哪些呢?
1. 产品质量问题

产品质量是一个老生常谈的问题,不仅仅是软件开发,其他产品莫不如此,相信被劣质产品坑过朋友不在少数,个中滋味恐怕只有自己能体会了。

而在软件开发行业,不规范的开发团队技术水平一定不会高到哪里去,开发流程简陋,虽然价格上有优势,但团队内部可能流程混乱,一人身兼数职,没有一个严格的项目管控流程,后期的使用过程中出现问题,想解决都找不到门路。这是因为不规范的开发团队,公司管理上也会出现一定的问题,留不住高技术人才,所以通常不具备开发高技术含量软件的实力。而且在项目实施的过程中很可能会出现为了节省成本导致实际的的交付结果与前期需求不符的情况,推延工期,甚至影响甲方产品上线规划,造成更大的资源损失。
2.安全隐患

企业信息安全是一个很严肃的问题,不成熟的技术软件在多变的互联网环境下很难保持稳定,产品bug很多,交付上线之后需要花费很长的时间去调试,严重影响到用户体验,而这种体验的而可怕之处在于会有导致平台用户的流失的风险,因为目前那么多的市场应用已经让用户挑花眼,体验不佳,哪怕是加载不够流畅都可能会导致用户放弃使用,得不偿失。

以公司主要产品learun为例,这是一个敏捷开发框架,主要帮助用户快速开发OA、ERP、CRM、移动app、电商后台等应用系统,仅前期开发就占据了十几名.net程序员半年多的时间,后期的测试等项目加在一起,足足一年的时间才完成1.0版本,但是后期的客户交付就很少出现问题,反馈最多的是让增加一些比较个性化的拓展功能,到现在为止9年时间,已经成功升级至7.0版本,口碑及使用者数量也在稳步提升,所以软件开发做好基本功是非常重要的。
3.售后问题

这个可能使用过的人最有感触,试想一下,一个不规范的开发团队,连技术都很难保证,服务也不可能好到哪里去,通常这类公司只能提供一次性售后服务,且不说这个售后是否便捷,售后不及时甚至推脱售后的也常有出现,最终买卖双方形成拉锯,既耽误开发时间,又耽误用户的业务进展。
继续阅读 »

进入互联网时代以来,企业的信息化建设也逐步进入了繁荣期,但是面对市场上纷繁多样的应用软件,选择便显得很重要了。

在企业市场的争夺中,软件开发的商场竞争也在不断升级,无数的软件公司倒下,又有更多年轻的公司崛起。

面对市场上参差不齐的开发公司,选择时必须要小心谨慎,因为如果不慎选择了不规范的外包公司或者技术能力有限的公司,不仅会让资金打水漂,更有可能打乱产品上线规划,造成公司的严重损失,所以说软件开发必须要筛选正规的软件公司,避免后期的使用风险。

那么具体来说风险会有哪些呢?
1. 产品质量问题

产品质量是一个老生常谈的问题,不仅仅是软件开发,其他产品莫不如此,相信被劣质产品坑过朋友不在少数,个中滋味恐怕只有自己能体会了。

而在软件开发行业,不规范的开发团队技术水平一定不会高到哪里去,开发流程简陋,虽然价格上有优势,但团队内部可能流程混乱,一人身兼数职,没有一个严格的项目管控流程,后期的使用过程中出现问题,想解决都找不到门路。这是因为不规范的开发团队,公司管理上也会出现一定的问题,留不住高技术人才,所以通常不具备开发高技术含量软件的实力。而且在项目实施的过程中很可能会出现为了节省成本导致实际的的交付结果与前期需求不符的情况,推延工期,甚至影响甲方产品上线规划,造成更大的资源损失。
2.安全隐患

企业信息安全是一个很严肃的问题,不成熟的技术软件在多变的互联网环境下很难保持稳定,产品bug很多,交付上线之后需要花费很长的时间去调试,严重影响到用户体验,而这种体验的而可怕之处在于会有导致平台用户的流失的风险,因为目前那么多的市场应用已经让用户挑花眼,体验不佳,哪怕是加载不够流畅都可能会导致用户放弃使用,得不偿失。

以公司主要产品learun为例,这是一个敏捷开发框架,主要帮助用户快速开发OA、ERP、CRM、移动app、电商后台等应用系统,仅前期开发就占据了十几名.net程序员半年多的时间,后期的测试等项目加在一起,足足一年的时间才完成1.0版本,但是后期的客户交付就很少出现问题,反馈最多的是让增加一些比较个性化的拓展功能,到现在为止9年时间,已经成功升级至7.0版本,口碑及使用者数量也在稳步提升,所以软件开发做好基本功是非常重要的。
3.售后问题

这个可能使用过的人最有感触,试想一下,一个不规范的开发团队,连技术都很难保证,服务也不可能好到哪里去,通常这类公司只能提供一次性售后服务,且不说这个售后是否便捷,售后不及时甚至推脱售后的也常有出现,最终买卖双方形成拉锯,既耽误开发时间,又耽误用户的业务进展。 收起阅读 »

【我最喜爱的 Cloud Studio 插件评选大赛】终于开始了!


2.jpg


由 环信、腾讯云和 CODING 共同举办的 我最喜爱的 Cloud Studio 插件评选大赛正式开始了!在这场比赛里,将会有技术上的碰撞,大牛评委的专业点评,愉快的技术交流,好玩的插件尝试。

6ccda21fgy1fxeim29mncj20ik0e6dn4.jpg

  • 参赛者可以围绕 Git、实用小工具、腾讯云产品对接、UI 强化、语言支持等 14 个主题提交插件,再加上最具娱乐奖,代码最简单奖,设置功能最复杂奖等;
  • 近 30 种奖项,超高中奖率;
  • 插件只要提交上架,就有 50 元的话费相赠;
  • 只要关注 CODING 公众号并转发活动讯息到朋友圈,即可获得手机充值小礼!


参与方式

注册并登陆腾讯云开发者平台https://dev.tencent.com) -> 点击进入活动页面 -> 点击进行插件的编写与提交(需要选择参与评选的类别)-> 审核无误后即可上架自动参与评选。

赛程时间
6ccda21fly1fxejmnr8oej20ow03odfy.jpg

 
环信特别奖
tb16@2x.png

基于环信开发一款聊天插件,即有机会获得特别奖,根据作品还将获得环信提供的神秘奖品
更多活动信息,请浏览我们的活动页面。

进入活动页面>
继续阅读 »

2.jpg


由 环信、腾讯云和 CODING 共同举办的 我最喜爱的 Cloud Studio 插件评选大赛正式开始了!在这场比赛里,将会有技术上的碰撞,大牛评委的专业点评,愉快的技术交流,好玩的插件尝试。

6ccda21fgy1fxeim29mncj20ik0e6dn4.jpg

  • 参赛者可以围绕 Git、实用小工具、腾讯云产品对接、UI 强化、语言支持等 14 个主题提交插件,再加上最具娱乐奖,代码最简单奖,设置功能最复杂奖等;
  • 近 30 种奖项,超高中奖率;
  • 插件只要提交上架,就有 50 元的话费相赠;
  • 只要关注 CODING 公众号并转发活动讯息到朋友圈,即可获得手机充值小礼!


参与方式

注册并登陆腾讯云开发者平台https://dev.tencent.com) -> 点击进入活动页面 -> 点击进行插件的编写与提交(需要选择参与评选的类别)-> 审核无误后即可上架自动参与评选。

赛程时间
6ccda21fly1fxejmnr8oej20ow03odfy.jpg

 
环信特别奖
tb16@2x.png

基于环信开发一款聊天插件,即有机会获得特别奖,根据作品还将获得环信提供的神秘奖品
更多活动信息,请浏览我们的活动页面。

进入活动页面> 收起阅读 »

asp.net强大后台:learun混合工作流框架规范



以前,我们对标准工作流进行过简单梳理,今天,我们再来看一下混合工作流。

了解混合工作流,我们必须要先分清角色、内容、流程之间的关系——即角色与内容是挂在流程节点上的功能点。在实际操作中,我们需要将流程节点控制好,再将不同的角色,以及对应的操作内容挂靠上去即可,这样一来是可以方便理清关系,另外也可以使系统更有层次。
控制好非标准流程节点,可以由以下几个方面着手。

1.如果流程配置者有配置SQL的能力,那么将数据库流程配置权限开放,让配置者自行配置,这样的开发工作压力会小一些。

2.画流程图的方式。一个流程的执行可以通过流程图来表现,对于产品经理来说是再熟悉不过了。通过流程图的基本逻辑,可以将流程中遇到的各种情况可视化的展示出来,条理清晰而且操作简单。缺点即开发难度过大,一般小团队难以胜任。

3.通过一一配置功能来进行配置,这种方式虽然表面上看起来十分的繁琐,但是相对于前两种来说开发难度小,且对于配置者的能力要求不高。

具体来说,要单独配置每一项功能的流程,先确定流程的主流程有几个节点,如果碰到判断的节点选择是,碰到并发流程或执行的节点选择最长的一个流程。确定之后,将所有节点的内容操作与角色配置出来,然后再配置该节点是否进行判断,是否进行或操作,是否进行与操作。

如果有判断操作时,则分出一个子流程,再将子流程按照上述方式进行配置,最终归于主流程的某一个节点。如果有与操作时,要确定配置与操作的分支节点时是要配置在单个节点还是多个节点。

单个节点的话则需满足这两个节点才往下进行,多个节点时则将这几个节点作为一个小流程单独按照上述方式进行配置再合并至主流程,看是否满足与行为。

如果有或操作判断时,同样要确定在哪个节点的或操作至哪个节点可以进行另外的节点流转。

以上这些情况对于开发团队来说也是一个巨大的考验,因为不同的工作流程代表着不同权限的操作,不同状态的流转,而可定制化的流程则代表着其中的变化无穷,对于服务器的压力,数据库的冗余情况都不容乐观。

那么,如何设计高效的混合工作流呢?

设计一个后台压力小,操作简单的高效混合工作流,可以有两种方式。

第一:将非标准工作流拆分成多个标准工作流。

第二:开辟独立与配置权限之外的工作流角色模块。

1. 将混合工作流拆分成多个标准工作流

一个非标准工作流固然麻烦,可是在大多数的情况下,其可以拆分为几个标准工作流。比如,某个非标准工作流可以线性拆分为多个分支流程,并发流程与执行、并发流程或执行。

将其每一个组合到一起,即可形成完整的工作流,那么我们就可以在系统中提供组合模板,让配置者可以进行选择,组合到一起形成一个非标准工作流。

如果是非线性的,比如可能为分支套分支,并发套并发的情况,我们可以将每一种情况都拆分成一个工作流,然后将生产端入口保持统一,每一步的不同操作可以进入不同的工作流,最终流转的出口保持一致即可。有点类似于开发中设计模式的工厂模式。

2. 开辟独立与配置权限之外的工作流角色模块

一般来说,我们在配置工作流角色的时候,都是使用类似权限控制的角色,比如到这个节点角色为库管,另一个节点角色为商管。其实换个角度想想,再说设计工作流的时候,完全可以设计一个独立于权限之外只配置工作流的角色。

比如“分支节点角色1号”“流程角色1号”“并发或角色2号”,然后再通过穷举法,将所需要用到的使用流程都列出来,把角色放置于节点上。

这样,一个活的需要配置的流程就变成了一个个的死流程。

再将这些角色赋予权限角色。再定义一些规则:比如若没有配置此节点的角色则此节点默认通过,将某个工作流角色配置两个权限角色则为或操作/与操作。这样也就解决了上述的问题。
工作流可以说是后台系统中比较复杂的一部分。即便某些系统中一开始没有工作流,随着系统功能的增加,也不可避免会用到工作流,所以提前了解下工作流的设计方法,对于产品来说很有帮助,在开始设计的阶段也可以考虑将内容设计进去以免后期维护成本过大。

官方:http://www.learun.cn/

免费体验地址:http://www.learun.cn/Home/VerificationForm
继续阅读 »


以前,我们对标准工作流进行过简单梳理,今天,我们再来看一下混合工作流。

了解混合工作流,我们必须要先分清角色、内容、流程之间的关系——即角色与内容是挂在流程节点上的功能点。在实际操作中,我们需要将流程节点控制好,再将不同的角色,以及对应的操作内容挂靠上去即可,这样一来是可以方便理清关系,另外也可以使系统更有层次。
控制好非标准流程节点,可以由以下几个方面着手。

1.如果流程配置者有配置SQL的能力,那么将数据库流程配置权限开放,让配置者自行配置,这样的开发工作压力会小一些。

2.画流程图的方式。一个流程的执行可以通过流程图来表现,对于产品经理来说是再熟悉不过了。通过流程图的基本逻辑,可以将流程中遇到的各种情况可视化的展示出来,条理清晰而且操作简单。缺点即开发难度过大,一般小团队难以胜任。

3.通过一一配置功能来进行配置,这种方式虽然表面上看起来十分的繁琐,但是相对于前两种来说开发难度小,且对于配置者的能力要求不高。

具体来说,要单独配置每一项功能的流程,先确定流程的主流程有几个节点,如果碰到判断的节点选择是,碰到并发流程或执行的节点选择最长的一个流程。确定之后,将所有节点的内容操作与角色配置出来,然后再配置该节点是否进行判断,是否进行或操作,是否进行与操作。

如果有判断操作时,则分出一个子流程,再将子流程按照上述方式进行配置,最终归于主流程的某一个节点。如果有与操作时,要确定配置与操作的分支节点时是要配置在单个节点还是多个节点。

单个节点的话则需满足这两个节点才往下进行,多个节点时则将这几个节点作为一个小流程单独按照上述方式进行配置再合并至主流程,看是否满足与行为。

如果有或操作判断时,同样要确定在哪个节点的或操作至哪个节点可以进行另外的节点流转。

以上这些情况对于开发团队来说也是一个巨大的考验,因为不同的工作流程代表着不同权限的操作,不同状态的流转,而可定制化的流程则代表着其中的变化无穷,对于服务器的压力,数据库的冗余情况都不容乐观。

那么,如何设计高效的混合工作流呢?

设计一个后台压力小,操作简单的高效混合工作流,可以有两种方式。

第一:将非标准工作流拆分成多个标准工作流。

第二:开辟独立与配置权限之外的工作流角色模块。

1. 将混合工作流拆分成多个标准工作流

一个非标准工作流固然麻烦,可是在大多数的情况下,其可以拆分为几个标准工作流。比如,某个非标准工作流可以线性拆分为多个分支流程,并发流程与执行、并发流程或执行。

将其每一个组合到一起,即可形成完整的工作流,那么我们就可以在系统中提供组合模板,让配置者可以进行选择,组合到一起形成一个非标准工作流。

如果是非线性的,比如可能为分支套分支,并发套并发的情况,我们可以将每一种情况都拆分成一个工作流,然后将生产端入口保持统一,每一步的不同操作可以进入不同的工作流,最终流转的出口保持一致即可。有点类似于开发中设计模式的工厂模式。

2. 开辟独立与配置权限之外的工作流角色模块

一般来说,我们在配置工作流角色的时候,都是使用类似权限控制的角色,比如到这个节点角色为库管,另一个节点角色为商管。其实换个角度想想,再说设计工作流的时候,完全可以设计一个独立于权限之外只配置工作流的角色。

比如“分支节点角色1号”“流程角色1号”“并发或角色2号”,然后再通过穷举法,将所需要用到的使用流程都列出来,把角色放置于节点上。

这样,一个活的需要配置的流程就变成了一个个的死流程。

再将这些角色赋予权限角色。再定义一些规则:比如若没有配置此节点的角色则此节点默认通过,将某个工作流角色配置两个权限角色则为或操作/与操作。这样也就解决了上述的问题。
工作流可以说是后台系统中比较复杂的一部分。即便某些系统中一开始没有工作流,随着系统功能的增加,也不可避免会用到工作流,所以提前了解下工作流的设计方法,对于产品来说很有帮助,在开始设计的阶段也可以考虑将内容设计进去以免后期维护成本过大。

官方:http://www.learun.cn/

免费体验地址:http://www.learun.cn/Home/VerificationForm 收起阅读 »

企业内部应用的核心与灵魂:工作流管理系统


工作流是企业内部系统的核心和灵魂,而审批则是工作流中的最基础的应用场景。在公司管理和运转中引入审批工作流,替代原本的纸质申请和审批,以期提高公司的运转效率公司管理制度规范化系统留档,便于追溯环保。
总结了在企业在实际业务中需求,根据客户反馈,构建出一套敏捷开发框架--learun。Learun可以保证在团队的开发过程中高效协作,同时覆盖OA工作流、ERP、CRM、HRM、MIS、BI、移动APP、电商后台等多项应用系统的配置,大幅节省开发成本,提升开发效率。更详细的可以在网站www.learun.cn体验。

一. 角色

在一个公司中,每个人都会有自己的岗位职责和层级之分,不同的岗位和层级定位不一样,需要完成的任务也不一样。在审批流程中,我们只抽象划分为两类:

角色1:发起人

审批的发起人需要完成的主要是事务性、操作性的工作,同时也是一个审批流程的Owner,是最关心审批进展的人。因此在发起人的角度,在创建完审批事项后,还需要完善相关信息、催促审批人及时审批、处理驳回修改意见、重新提交等。发起人角度设计的要点总结如下:

兼容统一发起入口和业务场景触发常用的审批事项要方便找到有统一汇总的审批管理页面

角色2:审批人

审批人在流程中需要完成的主要是决策性的工作,因此在审批人的视角,内容和操作都应该尽量精简:

只看到最重要的信息,避免信息过多影响判断只进行必要操作,不能有过多选择或过多输入,影响决策效率统一的页面进行审批操作和管理需要有审批历史,以便追溯

二. 内容

1. 提炼最小集合

根据审批事项的不同,需要流转的内容也是不同的。对于审批流程的设计来说,需要在实际业务中提炼出最核心的内容,一则可以减轻发起人的工作负担(发起一个审批要填一堆的资料相信没人会开心),二则可以提高决策的准确性和效率。

例如一个请假审批流程,核心就是请假时间、事由和请假类型;而一个立项投决的审批,则需要重点展示立项会的表决结果,同时还需要把会议记录做为附件带上,以便在必要时可以查看,在交互上,这里同样需要注意内容的归类、收纳。

设计要点总结如下:

内容尽可能精炼有些内容是必要的,但系统可以自行获取就不要让发起人再输入一遍预置常用的内容,用选择的方式替代输入的方式,同时也提高了内容的规范性

2. 查看和修改

在审批的过程中,有时候需要让不同的审批人查看不同的内容,且限定有些人有修改权限而有些人只有只读权限,这都会在后面的“权限”里总结。

三. 流程

1. 自主选定审批人流程

这是一种比较轻量、灵活的审批流程形式,适用于公司规模不大、流程没有标准化的情况。要点是发起人发起一个审批事项并提交时,需要自行选择下一个环节的审批人。而下一个环节的审批人审批通过后,可以选择继续流转到再下一个人去审批,或者结束这个流程。

2. 串行流程

串行流程就是每一个审批环节的人审批通过后,才会进入到下一个环节。每个环节的驳回,可以根据业务需要,设计成驳回到发起人、驳回到上一个环节或驳回到指定环节重新审批,或兼而有之,做为选项供审批人选择。
3. 并行流程

并行流程是一个审批环节需要几个人或角色审批通过才算通过,可以有以下两种方式:

任意一个人审批通过即进入下一环节必须所有人审批通过才进入下一环节

上述第一个方式比较好理解,第二个方式和串行流程容易混淆,即同样是要多个人审批,到底是一个接一个、还是同时通过才算通过?到底用哪种方式,区别是审批人是不是同一个级别,并行的方式其实类似于同级别的会签,而串行方式适合有上下层级关系的情况。

并行流程的驳回则相对简单,一般是设计成有一个人驳回则该环节即算驳回。
4. 条件触发流程

条件触发流程在审批工作流中也比较常见,设计上就是某个审批环节要由谁/或哪个角色审批,需要取决于条件判断。例如金额低于1万元由财务总监审批通过后即结束,金额在1万元以上则由副总裁审批通过后即结束。
5. 混合流程

混合流程顾名思义就是混合了以上几种流程,还是以上述金额审批为例,我们修改成:金额低于1万元的,由财务审批通过后即结束;金额在1万元到10万元的,需要先由财务审批,之后交由副总裁审批通过后即结束;金额高于10万元的,需要由董事长和总裁一同审批通过后才结束。
四. 动作

1. 通过

通过动作由审批人操作,是否需要输入通过原因、通过原因是否必填需要根据实际业务情况决定。要点总结如下:

简单申请不需要填写通过原因,或者原因选填通过原因需要填的话,可用于反馈或激励发起人的情况

2. 驳回修改

驳回修改动作由审批人操作,和通过不同,为了让发起人知道如何修改,驳回原因一般需要设定成必填项,否则发起人或上一个审批环节的人不知道为何被驳回、以及要如何修改。

驳回修改可根据业务需要,在以下逻辑中选择:

驳回到发起人驳回上一环节驳回到选定的之前的某个审批环节

3. 重新提交

重新提交由发起人操作,和驳回修改是一一对应的。设计上要注意,审批人审批重新提交的内容时,需要附带上一次驳回修改的原因。

4. 取消

取消动作可选,一般来说是发起人取消,而不是审批人取消,原因如下:

审批人只关心一个审批事务过来后,判断并决策是通过还是驳回取消和驳回含义容易混淆,区分不开

在设计上,我们还可以做到发起人是否可取消可由配置项进行配置。

五. 权限

权限的控制贯穿在审批流程的方方面面,上述的角色、内容、流程和动作都会涉及到权限的控制。权限体系的设计是一个大工程,在审批流程中,采用基于角色的访问控制体系(RBAC)是一个不错的选择:

“基于角色的访问控制体系,包括用户、角色、目标、操作、许可权五个基本数据元素,每个角色至少具备一个权限,每个用户至少扮演一个角色,可以对完全不同的角色分配完全相同的访问权限,用户和角色是多对多的关系。”

设计要点总结如下:

操作和许可权内容,可区分为功能权限和数据权限什么人可以发起什么审批,由功能权限控制什么人/角色在整个审批流程中可见什么数据,由数据权限控制什么人/角色可以审批什么环节,由独立的审批配置控制,下一节会进行阐述

关于权限可以参考笔者另一个篇文章:面向中小企业SaaS的权限管理系统

六. 配置和扩展性

审批工作流的配置灵活度和开发复杂度成反比,具体要灵活到什么程度,需要由业务需求决定。一般针对公司开发的中后台系统,灵活性相对较少,而面向多个公司的商业化的系统,则灵活性要求大大提高。配置的灵活性体现在以下方面:

审批流程的类型可修改具体的审批环节可增删改各个环节审批人/角色可配置审批相关的权限可变更

七. 效率

工作流的核心目标是提高企业运行效率,如果线上审批流程效率还不如原来的纸质操作,那这个流程的设计就是失败的,也失去了意义。因此,在完成整个审批流程的设计之后,我们还需要花大精力对流程的效率进行审视和优化。对于审批流程效率的提升,总结的经验点如下:

审批的操作尽可能精简,且操作含义明确只要求输入必要的表单待审批事项及时通知到审批人审批进展及时通知发起人发起人可选择主动催促审批人做好下一步操作的引导

总结

审批流程是中后台工作流的基础应用,我们在设计的过程中,把握的核心要点是“提高效率,规范管理”,在设计过程中要时时回头审视,以免脱离了这个最重要的目标。

免费演示地址:http://www.learun.cn/Home/VerificationForm
继续阅读 »

工作流是企业内部系统的核心和灵魂,而审批则是工作流中的最基础的应用场景。在公司管理和运转中引入审批工作流,替代原本的纸质申请和审批,以期提高公司的运转效率公司管理制度规范化系统留档,便于追溯环保。
总结了在企业在实际业务中需求,根据客户反馈,构建出一套敏捷开发框架--learun。Learun可以保证在团队的开发过程中高效协作,同时覆盖OA工作流、ERP、CRM、HRM、MIS、BI、移动APP、电商后台等多项应用系统的配置,大幅节省开发成本,提升开发效率。更详细的可以在网站www.learun.cn体验。

一. 角色

在一个公司中,每个人都会有自己的岗位职责和层级之分,不同的岗位和层级定位不一样,需要完成的任务也不一样。在审批流程中,我们只抽象划分为两类:

角色1:发起人

审批的发起人需要完成的主要是事务性、操作性的工作,同时也是一个审批流程的Owner,是最关心审批进展的人。因此在发起人的角度,在创建完审批事项后,还需要完善相关信息、催促审批人及时审批、处理驳回修改意见、重新提交等。发起人角度设计的要点总结如下:

兼容统一发起入口和业务场景触发常用的审批事项要方便找到有统一汇总的审批管理页面

角色2:审批人

审批人在流程中需要完成的主要是决策性的工作,因此在审批人的视角,内容和操作都应该尽量精简:

只看到最重要的信息,避免信息过多影响判断只进行必要操作,不能有过多选择或过多输入,影响决策效率统一的页面进行审批操作和管理需要有审批历史,以便追溯

二. 内容

1. 提炼最小集合

根据审批事项的不同,需要流转的内容也是不同的。对于审批流程的设计来说,需要在实际业务中提炼出最核心的内容,一则可以减轻发起人的工作负担(发起一个审批要填一堆的资料相信没人会开心),二则可以提高决策的准确性和效率。

例如一个请假审批流程,核心就是请假时间、事由和请假类型;而一个立项投决的审批,则需要重点展示立项会的表决结果,同时还需要把会议记录做为附件带上,以便在必要时可以查看,在交互上,这里同样需要注意内容的归类、收纳。

设计要点总结如下:

内容尽可能精炼有些内容是必要的,但系统可以自行获取就不要让发起人再输入一遍预置常用的内容,用选择的方式替代输入的方式,同时也提高了内容的规范性

2. 查看和修改

在审批的过程中,有时候需要让不同的审批人查看不同的内容,且限定有些人有修改权限而有些人只有只读权限,这都会在后面的“权限”里总结。

三. 流程

1. 自主选定审批人流程

这是一种比较轻量、灵活的审批流程形式,适用于公司规模不大、流程没有标准化的情况。要点是发起人发起一个审批事项并提交时,需要自行选择下一个环节的审批人。而下一个环节的审批人审批通过后,可以选择继续流转到再下一个人去审批,或者结束这个流程。

2. 串行流程

串行流程就是每一个审批环节的人审批通过后,才会进入到下一个环节。每个环节的驳回,可以根据业务需要,设计成驳回到发起人、驳回到上一个环节或驳回到指定环节重新审批,或兼而有之,做为选项供审批人选择。
3. 并行流程

并行流程是一个审批环节需要几个人或角色审批通过才算通过,可以有以下两种方式:

任意一个人审批通过即进入下一环节必须所有人审批通过才进入下一环节

上述第一个方式比较好理解,第二个方式和串行流程容易混淆,即同样是要多个人审批,到底是一个接一个、还是同时通过才算通过?到底用哪种方式,区别是审批人是不是同一个级别,并行的方式其实类似于同级别的会签,而串行方式适合有上下层级关系的情况。

并行流程的驳回则相对简单,一般是设计成有一个人驳回则该环节即算驳回。
4. 条件触发流程

条件触发流程在审批工作流中也比较常见,设计上就是某个审批环节要由谁/或哪个角色审批,需要取决于条件判断。例如金额低于1万元由财务总监审批通过后即结束,金额在1万元以上则由副总裁审批通过后即结束。
5. 混合流程

混合流程顾名思义就是混合了以上几种流程,还是以上述金额审批为例,我们修改成:金额低于1万元的,由财务审批通过后即结束;金额在1万元到10万元的,需要先由财务审批,之后交由副总裁审批通过后即结束;金额高于10万元的,需要由董事长和总裁一同审批通过后才结束。
四. 动作

1. 通过

通过动作由审批人操作,是否需要输入通过原因、通过原因是否必填需要根据实际业务情况决定。要点总结如下:

简单申请不需要填写通过原因,或者原因选填通过原因需要填的话,可用于反馈或激励发起人的情况

2. 驳回修改

驳回修改动作由审批人操作,和通过不同,为了让发起人知道如何修改,驳回原因一般需要设定成必填项,否则发起人或上一个审批环节的人不知道为何被驳回、以及要如何修改。

驳回修改可根据业务需要,在以下逻辑中选择:

驳回到发起人驳回上一环节驳回到选定的之前的某个审批环节

3. 重新提交

重新提交由发起人操作,和驳回修改是一一对应的。设计上要注意,审批人审批重新提交的内容时,需要附带上一次驳回修改的原因。

4. 取消

取消动作可选,一般来说是发起人取消,而不是审批人取消,原因如下:

审批人只关心一个审批事务过来后,判断并决策是通过还是驳回取消和驳回含义容易混淆,区分不开

在设计上,我们还可以做到发起人是否可取消可由配置项进行配置。

五. 权限

权限的控制贯穿在审批流程的方方面面,上述的角色、内容、流程和动作都会涉及到权限的控制。权限体系的设计是一个大工程,在审批流程中,采用基于角色的访问控制体系(RBAC)是一个不错的选择:

“基于角色的访问控制体系,包括用户、角色、目标、操作、许可权五个基本数据元素,每个角色至少具备一个权限,每个用户至少扮演一个角色,可以对完全不同的角色分配完全相同的访问权限,用户和角色是多对多的关系。”

设计要点总结如下:

操作和许可权内容,可区分为功能权限和数据权限什么人可以发起什么审批,由功能权限控制什么人/角色在整个审批流程中可见什么数据,由数据权限控制什么人/角色可以审批什么环节,由独立的审批配置控制,下一节会进行阐述

关于权限可以参考笔者另一个篇文章:面向中小企业SaaS的权限管理系统

六. 配置和扩展性

审批工作流的配置灵活度和开发复杂度成反比,具体要灵活到什么程度,需要由业务需求决定。一般针对公司开发的中后台系统,灵活性相对较少,而面向多个公司的商业化的系统,则灵活性要求大大提高。配置的灵活性体现在以下方面:

审批流程的类型可修改具体的审批环节可增删改各个环节审批人/角色可配置审批相关的权限可变更

七. 效率

工作流的核心目标是提高企业运行效率,如果线上审批流程效率还不如原来的纸质操作,那这个流程的设计就是失败的,也失去了意义。因此,在完成整个审批流程的设计之后,我们还需要花大精力对流程的效率进行审视和优化。对于审批流程效率的提升,总结的经验点如下:

审批的操作尽可能精简,且操作含义明确只要求输入必要的表单待审批事项及时通知到审批人审批进展及时通知发起人发起人可选择主动催促审批人做好下一步操作的引导

总结

审批流程是中后台工作流的基础应用,我们在设计的过程中,把握的核心要点是“提高效率,规范管理”,在设计过程中要时时回头审视,以免脱离了这个最重要的目标。

免费演示地址:http://www.learun.cn/Home/VerificationForm 收起阅读 »

这是一个悲伤的程序员爱情故事



        程序员的爱情,少了点浪漫,多了点bug。
        小马,科班出身,毕业后顺利进入了一家软件开发公司,可能天生就是干程序员的料,沉默寡言,但是遇到程序难题乐于钻研,誓死不休,人称拼命三郎,同事都觉得这个称呼太长,后来便逐步简化为“三郎”了。

        这家伙有一个怪癖,每次解决一个难题,总会对着电脑哈哈傻笑,把身边的人吓得一咯噔。刚开始大家都以为他脑子有问题,久而久之也就习惯了。

       幸福来得太突然,有一天,公司来了一位女程序员,乍一看,没有一点程序猿的样子,长发飘飘,一副大边框的眼睛里藏着一双美丽的大眼睛,连走路的风都带着阵阵清香。

        她被安排在了三郎身边,微笑着向三郎问好,随意但气质不减。但是就是这个简单的动作,让三郎不知所措,连刚才需要解决的bug都抛到九霄云外了。



        俗话说,近水楼台先得月,美女是刚毕业的小妹,工作中难免有许多问题需要问三郎,三郎也总是有求必应,很快帮她解决。美女进步很快,为了答谢三郎,决定请三郎吃个饭。这个不争气的家伙,有点怕,叫上了同事。

       饭桌上,几乎是美女一个在说话,或者说是美女在审问他,她问一句,他答一句。但三郎也是个男人,也有男人都有的想法,但却没有男子汉应该有的勇气,一直红着脸低头吃饭。最后忍不住想看她一眼,却差点噎着了。

      生活又在平淡中过了半年,一个晚上,他依然一个人在办公室里加班。突然她给她打了个电话,她说她喝多了,让他去接她。

      他见到她的时候,她是撑到了极限。如果他晚点到,她可能就睡地上了。

       她刚问完她的地址,她就醉倒了。她吃力的扶着她上了出租车,这也许是他第一次能如此近距离的和美女接触,能如此近距离的味着长发的味道。他费了好大的劲终于把她弄到了屋里,他自己也累得倒在了她边上。房间很静,他能听到她的心跳,能感受到她呼吸的空气撞到他脸上……。









他感觉自己心跳的频率比处理线上支付系统的Bug跳得还要快,他想尝试靠近她一点。就在这时,她吐了出来,弄得两个人一身污垢。

他给自己和她简单擦了一下,然后他拿出了电脑继续写代码。本来三天的工作量,他一个晚上就写完了,效率高得连他自己都不敢相信。

第二天她醒后,连连说谢谢。她说她和男友分手了,心情不好,然后自己跑出去喝酒了,要不是他还真不知道会发生什么事。

这以后,他工作总是心不在焉了。终于有一天,公司上了一个新项目,做一个通用敏捷开发框架,项目命名为“learun”(现在已经成为公司最赚钱的项目了),三郎和美女一起调到了这个项目,美女毕竟经验不丰富,在一个OA组件的构建上遇到了难题,要三郎帮他解决。他连续三天加班到凌晨,顺利帮她完成,而且在代码的注释里,表达了对她的喜欢,这是三郎能想到的最好的方式了。可是天不遂人愿,正好那天美女有事,直接便把这个模块提交了,上线后便没有改过。
        多少天,他一直希望那些地方再出一个bug,让她重新再看到她写给她的心里话。不过,他等了几个月,发现她和之前没什么区别,后来三郎就离职了,50%的工资涨幅都没能留住他。

       接下来的几年里,三郎又换了几家公司。

       今年,他到一家大公司面式的时候,最后的技术总监问他干技术这么多年,最让他揪心的事是什么。

      把他的这个故事和面试官说了。

     最后面试官问他这女孩子到现在有没有联系过你。

     他摇了摇头。

     面试官说了一句,你被录用了,这么多年,那女孩子都没有联系你,说明你写的代码很稳定。
继续阅读 »


        程序员的爱情,少了点浪漫,多了点bug。
        小马,科班出身,毕业后顺利进入了一家软件开发公司,可能天生就是干程序员的料,沉默寡言,但是遇到程序难题乐于钻研,誓死不休,人称拼命三郎,同事都觉得这个称呼太长,后来便逐步简化为“三郎”了。

        这家伙有一个怪癖,每次解决一个难题,总会对着电脑哈哈傻笑,把身边的人吓得一咯噔。刚开始大家都以为他脑子有问题,久而久之也就习惯了。

       幸福来得太突然,有一天,公司来了一位女程序员,乍一看,没有一点程序猿的样子,长发飘飘,一副大边框的眼睛里藏着一双美丽的大眼睛,连走路的风都带着阵阵清香。

        她被安排在了三郎身边,微笑着向三郎问好,随意但气质不减。但是就是这个简单的动作,让三郎不知所措,连刚才需要解决的bug都抛到九霄云外了。



        俗话说,近水楼台先得月,美女是刚毕业的小妹,工作中难免有许多问题需要问三郎,三郎也总是有求必应,很快帮她解决。美女进步很快,为了答谢三郎,决定请三郎吃个饭。这个不争气的家伙,有点怕,叫上了同事。

       饭桌上,几乎是美女一个在说话,或者说是美女在审问他,她问一句,他答一句。但三郎也是个男人,也有男人都有的想法,但却没有男子汉应该有的勇气,一直红着脸低头吃饭。最后忍不住想看她一眼,却差点噎着了。

      生活又在平淡中过了半年,一个晚上,他依然一个人在办公室里加班。突然她给她打了个电话,她说她喝多了,让他去接她。

      他见到她的时候,她是撑到了极限。如果他晚点到,她可能就睡地上了。

       她刚问完她的地址,她就醉倒了。她吃力的扶着她上了出租车,这也许是他第一次能如此近距离的和美女接触,能如此近距离的味着长发的味道。他费了好大的劲终于把她弄到了屋里,他自己也累得倒在了她边上。房间很静,他能听到她的心跳,能感受到她呼吸的空气撞到他脸上……。









他感觉自己心跳的频率比处理线上支付系统的Bug跳得还要快,他想尝试靠近她一点。就在这时,她吐了出来,弄得两个人一身污垢。

他给自己和她简单擦了一下,然后他拿出了电脑继续写代码。本来三天的工作量,他一个晚上就写完了,效率高得连他自己都不敢相信。

第二天她醒后,连连说谢谢。她说她和男友分手了,心情不好,然后自己跑出去喝酒了,要不是他还真不知道会发生什么事。

这以后,他工作总是心不在焉了。终于有一天,公司上了一个新项目,做一个通用敏捷开发框架,项目命名为“learun”(现在已经成为公司最赚钱的项目了),三郎和美女一起调到了这个项目,美女毕竟经验不丰富,在一个OA组件的构建上遇到了难题,要三郎帮他解决。他连续三天加班到凌晨,顺利帮她完成,而且在代码的注释里,表达了对她的喜欢,这是三郎能想到的最好的方式了。可是天不遂人愿,正好那天美女有事,直接便把这个模块提交了,上线后便没有改过。
        多少天,他一直希望那些地方再出一个bug,让她重新再看到她写给她的心里话。不过,他等了几个月,发现她和之前没什么区别,后来三郎就离职了,50%的工资涨幅都没能留住他。

       接下来的几年里,三郎又换了几家公司。

       今年,他到一家大公司面式的时候,最后的技术总监问他干技术这么多年,最让他揪心的事是什么。

      把他的这个故事和面试官说了。

     最后面试官问他这女孩子到现在有没有联系过你。

     他摇了摇头。

     面试官说了一句,你被录用了,这么多年,那女孩子都没有联系你,说明你写的代码很稳定。 收起阅读 »