정상에서 IT를 외치다

[Android, MVP, Dagger] MVP 기본 예제 - Dagger 사용하기 본문

안드로이드

[Android, MVP, Dagger] MVP 기본 예제 - Dagger 사용하기

Black-Jin 2019. 2. 8. 12:19
반응형


안녕하세요. 블랙진입니다.


지난시간 MVP 기본예제에 Room을 사용하여 최근 봤던 유저 목록을 저장해보았습니다. 이번시간에는 Dagger 을 사용하여 Presenter 을 외부에서 주입하도록 해보겠습니다. 현재 MVP 를 활용한 토이프로젝트를 총 3단계에 걸쳐서 리뷰를 진행하고 있습니다.


1. MVP 기본 예제

2. Room 사용하기

3. Dagger 사용하기


기존 프로젝트에서 MainActivity 만 Dagger 를 사용하여 리뷰해 보겠습니다. 


1. Dagger 설정하기

Dagger을 설정하는 방법은 포스팅을 확인해 주세요.



2. API Module 만들기

GithubApiProvider 을 사용하여 Api 를 불러왔습니다. 이를 Dagger 을 사용하여 변환해 보겠습니다.

Dagger 에서 모듈에 역활은 공급할 객체를 생성하는 것입니다.  그럼 아래 GithubAPiProvider 에서 사용한 객체를 공급해 보겠습니다.

public final class GithubApiProvider {

public static GithubApi provideGithubApi() {
return new Retrofit.Builder()
.baseUrl(Constant.BASE_URL)
// OkHttpClient 객체를 필요로 합니다.
.client(provideOkHttpClient(provideLoggingInterceptor()))
// CallAdapter.Factory 객체를 필요로 합니다.
.addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
// Converter.Factory 객체를 필요로 합니다.
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GithubApi.class);
}

private static OkHttpClient provideOkHttpClient(@NonNull HttpLoggingInterceptor interceptor) {
OkHttpClient.Builder b = new OkHttpClient.Builder();
b.addInterceptor(interceptor);
return b.build();
}

private static HttpLoggingInterceptor provideLoggingInterceptor() {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return interceptor;
}
}

provideGithubApi 함수는 OkhttpClient, CallAdapter.Factory, Converter.Factory 3가지 객체를 필요로 합니다. 이에 아래와 같이 ApiModule 을 만들어 줍니다.

@Module
class ApiModule {

@Provides
@Singleton
GithubApi provideGithubApi(
OkHttpClient okHttpClient,
CallAdapter.Factory callAdapter,
Converter.Factory factory
) {
return new Retrofit.Builder()
.baseUrl(Constant.BASE_URL)
.client(okHttpClient)
.addCallAdapterFactory(callAdapter)
.addConverterFactory(factory)
.build()
.create(GithubApi.class);
}

}

이제 OkhttpClient, CallAdapter.Factory, Converter.Factory 3가지 객체를 추가해줍니다.

@Provides
@Singleton
OkHttpClient provideOkHttpClient(HttpLoggingInterceptor interceptor) {
OkHttpClient.Builder b = new OkHttpClient.Builder();
b.addInterceptor(interceptor);
return b.build();
}

@Provides
@Singleton
//provideOkHttpClient 에 필요한 객체입니다.
HttpLoggingInterceptor provideLoggingInterceptor() {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return interceptor;
}

@Provides
@Singleton
CallAdapter.Factory provideCallAdapterFactory() {
return RxJava2CallAdapterFactory.createAsync();
}

@Provides
@Singleton
Converter.Factory provideConverterFactory() {
return GsonConverterFactory.create();
}

이렇게 ApiModule 을 만들어 주시면 됩니다.



3. AppModule 만들기

AppModule 에서는 어플리케이션의 컨텍스트 객체를 제공해 줄겁니다.

@Module
class AppModule {

// 애플리케이션의 컨텍스트를 제공합니다.
// 다른 컨텍스트와의 혼동을 방지하기 위해 "appContext"라는 이름으로 구분합니다.
@Provides
@Named("appContext")
@Singleton
Context provideContext(Application application) {
return application.getApplicationContext();
}
}



4. RoomModule 만들기

역시 아래 UserDatabaseProvider 에서의 역활을 Dagger 로 바꿔보겠습니다.

// 싱글톤 패턴을 사용하여 데이터베이스 객체를 제공합니다.
public class UserDatabaseProvider {

private volatile static UserDatabase INSTANCE = null;

private UserDatabaseProvider() {}

public static UserDatabase getInstance(Context context) {

if(INSTANCE == null) {
synchronized (UserDatabase.class) {
INSTANCE = Room.databaseBuilder(
context,
UserDatabase.class,
"black_jin.db")
.build();
}
}

return INSTANCE;
}
}

Dagger 변환을 위한 RoomModule

@Module
class RoomModule {

@Provides
@Singleton
UserDao provideUserDao(UserDatabase db) {
return db.getUserDao();
}

// 데이터베이스를 관리하는 객체인 UserDatabase 를 제공합니다.
// "appContext" 라는 이름으로 구분되는 Context 객체를 필요로 합니다.
@Provides
@Singleton
UserDatabase provideUserDatabase(@Named("appContext") Context context) {
return Room.databaseBuilder(context, UserDatabase.class,
"black_jin.db")
.build();
}
}


5. AppComponent 만들기

Component 는 사용할 모듈을 불러오고 어디로 주입할지 결정합니다.

// AppComponent 를 선언하면서 컴포넌트로 묶어둘 모듈을 추가합니다.
// 대거의 안드로이드 지원 모듈인 AndroidSupportInjectionModule 을 함께 추가합니다.
// AppComponent 는 AndroidInjector 인터페이스를 상속하도록 하며,
// 애플이케이션을 상속한 클래스인 MyApplication 을 타입 인자로 넣어줍니다.
//
// 대거의 안드로이드 특화 기능에 대해 더 자세한 내용이 궁금하신 분은 공식 홈페이지에서 확인해주세요
// https://google.github.io/dagger/android.html
@Singleton
@Component(modules = {
AppModule.class, ApiModule.class, RoomModule.class,

AndroidSupportInjectionModule.class,
ActivityBinder.class
})
interface AppComponent extends AndroidInjector<MyApplication> {

// AppComponent 를 생성할 때 사용할 빌더 클래스를 정의합니다.
@Component.Builder
interface Builder {

// @BindsInstance 어노테이션으로 객체 그래프에 추가할 객체를 선언합니다.
// 객체 그래프에 추가할 객체를 인자로 받고, 빌더 클래스를 반환하는 함수 형태로 선언합니다.
@BindsInstance
Builder application(Application app);

// 빌더 클래스는 컴포넌트를 반환하는 build() 함수를 반드시 포함해야 합니다.
AppComponent build();
}
}



6. ActivityBInder 만들기

/**
* 객체 그래프에 추가할 엑티비티는 해당 엑티비티를 반환하는 함수에
* @ContributesAndroidInjector 어노테이션을 추가하여 선언합니다.
*/
@Module
abstract class ActivityBinder {

@ContributesAndroidInjector
abstract MainActivity bindMainActivity();

@ContributesAndroidInjector
abstract DetailActivity bindDetailActivity();

@ContributesAndroidInjector
abstract RecentActivity bindRecentActivity();
}

AcitvityBinder 를 통해 객체 그래프에 엑티비티를 추가해 줄 수 있습니다. 

위 예제에서는 Main, Detail, Recent 3개의 엑티비티가 있기 때문에 위와 같이 추가해 줍니다. 이렇게 함으로서 원하는 특정 모듈을 각 엑티비티에 주입시킬 수 있게 됩니다.



7. MyApplication 만들고 AndroidManifest 등록하기

// DaggerApplication 을 상속합니다.
public class MyApplication extends DaggerApplication {

// DaggerAppComponent 의 인스턴스를 반환합니다.
@Override
protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
return DaggerAppComponent.builder().application(this).build();
}
}

 DaggerApplication 만들고 Manifest 에 등록을 해줍니다. 이렇게 하면 ActivityBinder 에서 선언해주 Activity 에 AppComponent 에서 정한 모듈을 주입할 수 있게 됩니다.



8. BaseActivity 변경하기

각 Activity 에서 AppCompatActivity 를 상속하는 BaseActivity 를 사용했습니다. BaseaActivity 의 상속 값을 DaggerAppCompatActivity 로 변경해 줍니다.

public abstract class BaseActivity extends DaggerAppCompatActivity {

@Override
public void setContentView(int layoutResID) {
super.setContentView(layoutResID);
ButterKnife.bind(this);
}

}



9. MainActivity 에 Dagger 사용하기

GithubApi api = GithubApiProvider.provideGithubApi();

UserDao userDao = UserDatabaseProvider.getInstance(this).getUserDao();

MainActivity 에서 api 와 userDao 를 주입하여 사용할 수 있게 됩니다.

@Inject
GithubApi api;

@Inject
UserDao userDao;

@Inject 를 사용하여 위와 같이 사용할 수 있게 됩니다.



10 MainPresenter 주입하기

여기서 더 나아가 MainPresenter 또한 선언이 아닌 주입으로 해결해 보겠습니다. 6번에 ActivityBInder 를 통해 원하는 모듈을 원하는 Activity 에 주입할 수 있다고 했습니다.  그렇게 하기 위해 MainModule 을 만들어서 MainActivity에만 주입해 보겠습니다.

@Module
public class MainModule {

@Provides
MainAdapter provideMainAdapter(MainActivity activity) {
MainAdapter adapter = new MainAdapter();
adapter.setClickListener(activity);
return adapter;
}

@Provides
MainPresenter provideMainPresenter(MainActivity activity) {
MainPresenter presenter = new MainPresenter();
presenter.setView(activity);
return presenter;
}
}

MainActivity 에서 사용하는 MainPresenter 와 MainAdapter 을 주입하여 사용해 보겠습니다. 이미 MainAcitivty 도 ActivityBinder 에서 모듈로 선언했고 같은 Componet 안에 있기 때문에 주입이 되므로 위와같이 코딩할 수 있습니다

@ContributesAndroidInjector(modules = {MainModule.class})
abstract MainActivity bindMainActivity();

이렇게 한 후 ActivityBinder 에 MainModule 을 추가해 줍니다. 그러면 adapter 와 presenter 또한 대거를 통해 사용할 수 있게 됩니다.

@Inject
MainAdapter adapter;

@Inject
MainPresenter presenter;



11. Presenter 에서 UserDao, GithubApi 사용하기

UserDao, GithubApi는 MainAcitivty 에서 주입할 필요 없이 Presenter 에 주입하여 사용하도록 수정해 보겠습니다. 이를 위해서는 MainModule 에서 공급할 객체를 생성해 주어야 합니다.

@Provides
MainPresenter provideMainPresenter(MainActivity activity, UserDao userDao, GithubApi api) {
MainPresenter presenter = new MainPresenter(userDao, api);
presenter.setView(activity);
return presenter;
}

ProvideMainPresenter 에서 UserDao 와 GithubApi 를 추가 공급한 후 Presenter 에 넣어 주었습니다.

private UserDao userDao;

private GithubApi api;

public MainPresenter(UserDao userDao, GithubApi api) {
this.disposable = new CompositeDisposable();
this.userDao = userDao;
this.api = api;
}

MainPresenter 에서 생성자를 사용해 UserDao 와 GithubApi 또한 외부에서 주입 후 객체 선언에 초기화 하도록 수정할 수 있습니다.

아래는 변환 후 MainAcitivty 의 모습니다.

public class MainActivity extends BaseActivity 
implements MainContract.View, MainAdapter.OnItemClickListener {

@Inject
MainAdapter adapter;

@Inject
MainPresenter presenter;

@BindView(R.id.recycler_view)
RecyclerView recyclerView;

@BindView(R.id.progress_view)
ProgressBar progressBar;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle("RANDOM USER");

recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
adapter.setClickListener(this);

presenter.setView(this);

presenter.loadData();
presenter.setRxEvent();
}

@Override
protected void onDestroy() {
super.onDestroy();
presenter.releaseView();
}

@Override
public void onClick(User user) {

// UserDatabase 에 저장합니다.
presenter.addUser(user);

// DetailActivity 로 이동합니다.
Intent intent = new Intent(this, DetailActivity.class);
intent.putExtra(DetailActivity.KEY_USER, user);
startActivity(intent);
}

@Override
public void showProgress() {
progressBar.setVisibility(View.VISIBLE);
}

@Override
public void hideProgress() {
progressBar.setVisibility(View.GONE);
}

@Override
public void setItems(ArrayList<User> items) {
adapter.setItems(items);
}

@Override
public void updateView(User user) {
adapter.updateView(user);
}

@OnClick(R.id.btn_recent_user)
void onClick() {
Intent intent = new Intent(this, RecentActivity.class);
startActivity(intent);
}
}

주요 객체는 모두 외부에서 주입하게 되었고 비즈니스 로직에 필요한 함수는 모두 Presenter 로 옮기게 되었습니다.


MainContract

public interface MainContract {

interface View extends BaseContract.View {

void showProgress();

void hideProgress();

void setItems(ArrayList<User> items);

void updateView(User user);

}

interface Presenter extends BaseContract.Presenter<View> {

@Override
void setView(View view);

@Override
void releaseView();

void loadData();

void setRxEvent();

void addUser(User user);
}
}



MainPresenter

public class MainPresenter implements MainContract.Presenter {

private MainContract.View view;

private CompositeDisposable disposable;

private UserDao userDao;

private GithubApi api;

public MainPresenter(UserDao userDao, GithubApi api) {
this.disposable = new CompositeDisposable();
this.userDao = userDao;
this.api = api;
}

@Override
public void setView(MainContract.View view) {
this.view = view;
}

@Override
public void releaseView() {
disposable.clear();
}

@Override
public void loadData() {

disposable.add(api.getUserList(Constant.RANDOM_USER_URL)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(__ -> {
view.showProgress();
})
.doOnTerminate(() -> {
view.hideProgress();
})
.subscribe(userResponse -> {
view.setItems((ArrayList<User>)userResponse.userList);
}, error -> {
Log.e("MyTag",error.getMessage());
})
);

}


@Override
public void addUser(User user) {

disposable.add(
Observable.just(user)
.subscribeOn(Schedulers.io())
.subscribe(
item -> {
Log.d("MyTag","item : " + item + " 저장");
userDao.add(item);
}
)
);

}

@Override
public void setRxEvent() {

disposable.add(
RxEvent.getInstance()
.getObservable()
.subscribe(
object -> {
if(object instanceof User) {
view.updateView((User) object);
}
}
)
);
}
}

이렇게 MainActivity 의 함수들을 Dagger 를 통해 주입하여 변경해 보았습니다.

처음 설정 및 집입장벽이 높아 보이지만 코드가 점점 커질 수록 이러한 Dagger 의 활용은 코드를 유지보수하고 한눈에 관찰하기에는 더욱 좋은 구조가 될 거 라고 생각합니다. 


위 예제 코드는 링크 에서 확인하실 수 있습니다.


1. MVP 기본 예제

2. Room 사용하기

3. Dagger 사용하기


<참고자료>

 mvp-dagger-architecture : 다른 방법으로 MVP를 만든 예제 입니다.

커니의 코틀린 : 주석에 쓰인 설명은 주로 커니의 코틀린 책에서의 예제를 참조하였습니다.


반응형
Comments