Dagger2+Retrofit+RxJava+Butterknife搭建项目

上一篇我们讲解了安卓中的MVP模型,这次我们将用查看学生列表作为例子,搭建一个MVP安卓项目。我们都知道不要重复造轮子,因此使用一些现成的框架会简化我们的操作。因此这篇文章将会重点介绍Dagger2+Retrofit+RxJava+Butterknife环境的搭建,以及如何使用。
项目代码连接

  • Dagger2 - 依赖注入
  • Retrofit - 发送http请求, 一种 type-­safe REST client
  • RxJava - 实现异步操作
    (个人觉得还是很复杂的,需要透彻理解观察者模式。可以看这篇博客稍微了解一下这个框架)
  • Butterknife - 用来绑定Android views和方法

添加依赖

目前我使用的gradle版本是2.3.2,引入Dagger2的时候已经不再需要配置apt插件。另外,配置Retrofit的各种converter和adapter时版本一定要一致。

    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.squareup.retrofit2:converter-gson:2.1.0'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
    compile 'com.jakewharton:butterknife:8.4.0'
    compile 'com.google.dagger:dagger:2.4'
    compile 'io.reactivex:rxjava:1.0.14'
    compile 'io.reactivex:rxandroid:1.0.1'
    compile 'io.reactivex:rxjava-joins:0.22.0'
    //偷懒用的现成的底部导航栏,很少女很好看
    compile 'com.ashokvarma.android:bottom-navigation-bar:0.9.5' 
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.4'

Model

因为要展示学生列表,所以首先定义从服务器端接收过来的学生信息。
服务器端提供接口:
/students

[
  {
    username:tj,
    name:"jenny",
    gender:"female",
    email:"test@163.com"
  }
]

Student类:

public class Student implements Serializable {
    @SerializedName("username")
    private String username;
    @SerializedName("name")
    private String name;
    @SerializedName("gender")
    private String gender;
    @SerializedName("email")
    private String email;
}

注意@SerializedName注解中要与服务器端提供的格式相符
因为要访问网络,所以记得在AndroidManifest.xml中添加INTERNET permissions

创建Retrofit实例

为了发送网络请求,我们需要使用Retrofit Builder 类并配置base URL。为了接收服务器端的json格式的数据并转化成对象,我们需要添加GsonConverterFactory。为了支持RxJava,Retrofit本来默认返回的是Call<T>,我们需要修改为Observable<T>,我们需要添加RxJavaCallAdapterFactory。

public class ApiClient {
    public static final String BASE_URL = "http://和谐掉/api/";
    private static Retrofit retrofit = null;


    public static Retrofit getClient() {
        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                    .build();
        }
        return retrofit;
    }
}

定义接收端口

在接口内定义接收端口,使用特殊的Retrofit注解来对请求参数和请求方法进行编码。
另外,服务器端获取信息的接口需要身份验证,所以需要在请求的Header中添加token。

public interface ApiInterface {
    @GET("students")
    Observable<List<Student>> getStudents(@Header("Authorization") String token);
}

Retrofit的其他注解
@Path - 用来代替API端口中的变量。如:

@GET("group/{groupId}/students")
Observable<List<User>> getStudents( @Path("groupId") int groupId);

@Query - 使用注解参数的值指定查询键名称。
@Body - POST 请求中的playload

在Repository中调用

public class StudentRepository {
    private static StudentRepository instance;
    protected ApiInterface apiService;

    private StudentRepository() {
        apiService = ApiClient.getClient().create(ApiInterface.class);
    }

    public static StudentRepository getInstance() {
        if (instance == null)
            instance = new TeacherRepository();
        return instance;
    }

    public Observable<List<Student>> getStudents(String username, String password) {
        return apiService.getStudents(getToken(username, password), groupId);
    }
}

Retrofit 会在后台的线程下载并转换数据。


Presenter

Presenter需要覆盖View的生命周期,如果我们需要跟踪Activity或者Fragment的生命周期,我们需要调用Presenter对应的方法。因此每个Presenter需要实现以下接口:

public interface BasePresenter<T> {
    void onCreate();

    void onStart();

    void onStop();

    void onPause();
}

StudentListPresenter

public class StudentListPresenter implements BasePresenter<BasicListView> {
    private Subscription subscription;
    private BasicListView view;
    private StudentRepository repository;

    public StudentListPresenter(BasicListView view, StudentRepository repository) {
        this.view = view;
        this.repository = repository;
    }

    public void getStudents(String username, String password) {
         subscription = repository.getStudents(username, password)
                .subscribeOn(Schedulers.io())
                .onErrorReturn(new Func1<Throwable, List<User>>() {
                    @Override
                    public List<User> call(Throwable throwable) {
                        throwable.printStackTrace();
                        view.showError();
                        return null;
                    }
                })
                .subscribe(new Action1<List<User>>() {
                    @Override
                    public void call(List<User> groups) {
                        if (groups != null) {
                            view.addStudents(groups);
                        }
                    }
                });
    }

    @Override
    public void onStop() {
        if (subscription != null && !subscription.isUnsubscribed()) {
            subscription.unsubscribe();
        }
    }
}

我们使用StudentRepository来获得学生列表,Presenter并不在意它是如何获得的,它只是简单地订阅Repository返回的Observable,更新view。为了避免后台任务完成前Activity就被销毁,Presenter需持有一个Subscription的引用,在Activity调用onStop()方法时unsubscribe.


View

根据依赖倒置原则,在View和Presenter之间定义一个接口,Activity或者Fragment实现该接口,Presenter持有接口的引用。

public interface BasicListView {
    void showError();

    void addStudents(List<User> list);
}

定义Activity或者Fragment实现该接口

public class StudentsListFragment extends BaseFragment implements BasicListView {
    @BindView(R.id.list)
    ListView listView;
    @Inject
    StudentListPresenter presenter;
    private View view;
    private UserInfoActivity activity;
    private List<Map<String, Object>> studentMap;

    @Override
    public void showError() {
        Runnable myRunnable = new Runnable() {
            @Override
            public void run() {
                Toast.makeText(activity, R.string.network_error, Toast.LENGTH_LONG).show();
            }
        };
        activity.runOnUiThread(myRunnable);
    }

    @Override
    public void addStudents(List<User> list) {
         for (User user : list) {
              Map<String, Object> map = new HashMap<>();
              map.put("title", user.getName());
              map.put("content", user.getNumber());
              studentMap.add(map);
          }
          showList();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        activity = (UserInfoActivity) getActivity();
        view = inflater.inflate(R.layout.student_list, container, false);
        ButterKnife.bind(this, view);
        DaggerActivityComponent.builder()
                .mainModule(new MainModule(this))
                .build()
                .inject(this);
        presenter.getStudents(username, password);
        return view;
    }

    @Override
    public void onStop() {
        super.onStop();
        presenter.onStop();
    }

    private void showList() {
        Runnable myRunnable = new Runnable() {
            @Override
            public void run() {
                String[] from = {"title", "content"};
                int[] to = {R.id.title, R.id.content};
                SimpleAdapter adapter = new SimpleAdapter(view.getContext(), studentMap, R.layout.short_student_info, from, to);
                listView.setAdapter(adapter);
            }
        };
        activity.runOnUiThread(myRunnable);
    }

}

在StudentsListFragment中,我们使用到了@BindView注解,该注解可以通过id找到对应的view组件。Butterknife的功能不止于此,其官方文档写的非常清楚,感兴趣请戳Butter Knife.

在MVP模型中,Activity或者Fragment持有Presenter的引用,并在Activity中实例化这个Presenter,即Activity依赖Presenter,Presenter又需要依赖View接口,从而更新UI。这样Activity与Presenter就耦合在了一起,因此我们这里使用到了Dagger2的依赖注入,来降低耦合。

让我们来看一下代码。首先我们声明了一个StudentListPresenter,在声明的基础上加了一个注解@Inject,表明Presenter是需要注入到Fragment中。这里要注意的是,使用@Inject时,不能用private修饰符修饰类的成员属性。

@Component(modules = MainModule.class)
public interface ActivityComponent {
    void inject(StudentsListFragment fragment);
}

然而,StudentListPresenter的构造函数和StudentListFragment中声明的presenter并不会凭空建立起联系。Component是一个接口或者抽象类,用来将它们连接在一起。
@Component注解标注,我们在这个接口中定义了一个inject()方法,参数是StudentListFragment。然后rebuild一下项目,会生成一个以Dagger为前缀的Component类,这里是DaggerActivityComponent.
注意StudentListFragment中的这段代码:

 DaggerActivityComponent.builder()
                .mainModule(new MainModule(this))
                .build()
                .inject(this);

此时Component就将@Inject注解的presenter与其构造函数联系了起来。

有时候,有的类没有构造函数,这些类无法用@Inject标注,比如第三方类库,系统类,以及上面示例的View接口。此时我们需要@Module标记的MainModlue类来提供依赖。
MainModule

@Module
public class MainModule {
    private StudentsListFragment fragment;

    public MainModule(StudentsListFragment fragment) {
        this.fragment = fragment;
    }

    @Provides
    public StudentsListFragment provideFragment() {
        return fragment;
    }

    @Provides
    public StudentRepository provideRepository() {
        return StudentRepository.getInstance();
    }

    @Provides
    public StudentListPresenter getStuListPresenter(StudentsListFragment fragment, TeacherRepository repository) {
        return new StudentListPresenter(fragment, repository);
    }
}

Module要发挥作用,还是要依靠于Component类,一个Component类可以包含多个Module类,用来提供依赖。

我们接着来看上面这段代码。这里通过new MainModule(this)将view传递到MainModule里。当去实例化StudentListPresenter时,发现构造函数有两个参数,此时会在Module里查找提供这个依赖的方法,即用@Provides标注的方法,将该View和Repository传递进去,这样就完成了presenter里View的注入。


讲到这里项目就搭建完毕啦,也实现了一个小功能。这里就略去layout的编写了毕竟我也不会。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容