Dagger 2: DI eficiente em Android - TDC 2015

Post on 12-Aug-2015

111 views 47 download

Transcript of Dagger 2: DI eficiente em Android - TDC 2015

Dagger 2Injeção de Dependência no Android

rafaeltoledo.net

@_rafaeltoledo

Desenv. Android @ Concrete Solutions

O que é Injeção de Dependência?

“Injeção de Dependência (ou Dependency Injection, em inglês) é um padrão de desenvolvimento de programas de

computadores utilizado quando é necessário manter baixo o nível de acoplamento entre

diferentes módulos de um sistema. (...)”

“Nesta solução as dependências entre os módulos não são definidas

programaticamente, mas sim pela configuração de uma infraestrutura de

software (container) que é responsável por "injetar" em cada componente suas

dependências declaradas. A Injeção de dependência se relaciona com o padrão Inversão de controle mas não pode ser

considerada um sinônimo deste.”

Wikipedia, 2015

?????????

Diz respeito a separação de onde os objetos são criados e onde são utilizados

Em vez de criar, você pede

class UserController {

void doLogic() {

try {

User user = RetrofitApi.getInstance().getUser(1);

new UserDaoImpl().save(user);

Logger.getForClass(UserController.class).log("Success");

} catch (IOException e) {

Logger.getForClass(UserController.class).logException(e);

}

}

}

class UserController {

UserDaoImpl dao;

Logger logger;

UserApi api;

void doLogic() {

try {

api = RetrofitApi.getInstance();

User user = api.getUser(1);

dao = new UserDaoImpl();

dao.save(user);

logger = Logger.getForClass(UserController.class);

logger.log("Success");

} catch (IOException e) {

logger = Logger.getForClass(UserController.class);

logger.logException(e);

}

}

}

class UserController {

UserDao dao; // Interface!

Logger logger;

Api api;

void doLogic() {

try {

if (api == null) api = RetrofitApi.getInstance();

User user = api.getUser(1);

if (dao == null) dao = new UserDaoImpl();

dao.save(user);

if (logger == null) logger = Logger.getForClass(UserController.class);

logger.log("Success");

} catch (IOException e) {

if (logger == null) logger = Logger.getForClass(UserController.class);

logger.logException(e);

}

}

}

class UserController {

UserDao dao = new UserDaoImpl();

Logger logger = Logger.getForClass(UserController.class);

Api api = RetrofitApi.getInstance();

void doLogic() {

try {

User user = api.getUser(1);

dao.save(user);

logger.log("Success");

} catch (IOException e) {

logger.logException(e);

}

}

}

class UserController {

UserDao dao;

Logger logger;

Api api;

public UserController() {

dao = new UserDaoImpl();

api = RetrofitApi.getInstance();

logger = Logger.getForClass(UserController.class);

}

void doLogic() {

try {

User user = api.getUser(1);

dao.save(user);

logger.log("Success");

} catch (IOException e) { logger.logException(e); }

}

}

class UserController {

UserDao dao;

Logger logger;

Api api;

public UserController() {

dao = new UserDaoImpl();

api = RetrofitApi.getInstance();

logger = Logger.getForClass(UserController.class);

}

void doLogic() {

try {

User user = api.getUser(1);

dao.save(user);

logger.log("Success");

} catch (IOException e) { logger.logException(e); }

}

}

class UserController {

UserDao dao;

Logger logger;

Api api;

public UserController() {

dao = new UserDaoImpl();

api = RetrofitApi.getInstance();

logger = Logger.getForClass(UserController.class);

}

void doLogic() {

try {

User user = api.getUser(1);

dao.save(user);

logger.log("Success");

} catch (IOException e) { logger.logException(e); }

}

}

class UserController {

UserDao dao;

Logger logger;

Api api;

public UserController() {

dao = new UserDaoImpl();

api = RetrofitApi.getInstance();

logger = Logger.getForClass(UserController.class);

}

void doLogic() {

try {

User user = api.getUser(1);

dao.save(user);

logger.log("Success");

} catch (IOException e) { logger.logException(e); }

}

}

UserDao

UserController

UserDao

UserController

SessionManager

UserDao

UserController

SessionManagerSignInController

UserDao

UserController

SessionManagerSignInController

CookieJob

JobManager

JobController

PermissionChecker

ReminderJob

RoleController

CandyShopper

FruitJuicerJob

UnknownController

UserDao

UserController

SessionManagerSignInController

CookieJob

JobManager

JobController

PermissionChecker

ReminderJob

RoleController

CandyShopper

FruitJuicerJob

UnknownController

EM TODO LUGAR!

class UserDaoImpl implements UserDao {

public UserDaoImpl() {

//...

}

}

class UserDaoImpl implements UserDao {

public UserDaoImpl(Context context) {

//...

}

}

Alteração em todas as classes!

Alteração em todas as classes!

Muito retrabalho!

Alteração em todas as classes!

Muito retrabalho!

Alteração em todas as classes!

Muito retrabalho!

class UserController {

UserDao dao;

Logger logger;

Api api;

public UserController() {

dao = new UserDaoImpl();

api = RetrofitApi.getInstance();

logger = Logger.getForClass(UserController.class);

}

void doLogic() {

try {

User user = api.getUser(1);

dao.save(user);

logger.log("Success");

} catch (IOException e) { logger.logException(e); }

}

}

class UserController {

UserDao dao;

Logger logger;

Api api;

public UserController() {

dao = new UserDaoImpl();

api = RetrofitApi.getInstance();

logger = Logger.getForClass(UserController.class);

}

void doLogic() {

try {

User user = api.getUser(1);

dao.save(user);

logger.log("Success");

} catch (IOException e) { logger.logException(e); }

}

}

class UserController {

UserDao dao;

Logger logger;

Api api;

public UserController(UserDao dao, Api api, Logger logger) {

this.dao = dao;

this.api = api;

this.logger = logger;

}

void doLogic() {

try {

User user = api.getUser(1);

dao.save(user);

logger.log("Success");

} catch (IOException e) { logger.logException(e); }

}

}

DI: Separação do uso e criação de objetos

DI: Separação do uso e criação de objetos

não há a necessidade de bibliotecasou frameworks!

Desvantagem: muito boilerplate

Spring, Guice, Dagger 1 & cia

#oldbutgold

Spring, Guice, Dagger 1 & cia

#oldbutnot

Dagger 2

Dagger 2?

Dagger 2

● Fork do Dagger, da Square, feito pela Google

● Elimina todo o uso de reflections

● Construída sobre as anotações javax.inject da especificação JSR-330

Dagger 2

● Validação de todo o grafo de dependências em tempo de compilação

● Menos flexível, se comparado ao Dagger 1 - ex.: não possui module overriding

● Proguard configuration-free

Dagger 2

● API enxuta!

● Código gerado é debugger-friendly

● Muito performático

● google.github.io/dagger

public @interface Component {

Class<?>[] modules() default {};

Class<?>[] dependencies() default {};

}

public @interface Subcomponent {

Class<?>[] includes() default {};

}

public @interface Module {

Class<?>[] includes() default {};

}

public @interface Provides {

}

public @interface MapKey {

boolean unwrapValue() default true;

}

public interface Lazy<T> {

T get();

}

// JSR-330

public @interface Inject {

}

public @interface Scope {

}

public @interface Qualifier {

}

Como integro no meu projeto?

// build.gradle

buildscript {

repositories {

jcenter()

}

dependencies {

classpath 'com.android.tools.build:gradle:1.2.3'

}

}

allprojects {

repositories {

jcenter()

}

}

// build.gradle

buildscript {

repositories {

jcenter()

}

dependencies {

classpath 'com.android.tools.build:gradle:1.2.3'

}

}

allprojects {

repositories {

jcenter()

}

}

// build.gradle

buildscript {

repositories {

jcenter()

}

dependencies {

classpath 'com.android.tools.build:gradle:1.2.3'

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.6'

}

}

allprojects {

repositories {

jcenter()

}

}

// build.gradle

buildscript {

repositories {

jcenter()

}

dependencies {

classpath 'com.android.tools.build:gradle:1.2.3'

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.6'

}

}

allprojects {

repositories {

jcenter()

}

}

apply plugin: 'com.android.application'

android {

compileSdkVersion 22

buildToolsVersion '22.0.1'

defaultConfig {

applicationId 'net.rafaeltoledo.tdc2015'

minSdkVersion 15

targetSdkVersion 22

versionCode 1

versionName '1.0'

}

}

dependencies {

compile 'com.android.support:appcompat-v7:22.2.1'

}

apply plugin: 'com.android.application'

apply plugin: 'com.neenbedankt.android-apt'

android {

compileSdkVersion 22

buildToolsVersion '22.0.1'

defaultConfig {

applicationId 'net.rafaeltoledo.tdc2015'

minSdkVersion 15

targetSdkVersion 22

versionCode 1

versionName '1.0'

}

}

dependencies {

compile 'com.android.support:appcompat-v7:22.2.1'

compile 'com.google.dagger:dagger:2.0.1'

apt 'com.google.dagger:dagger-compiler:2.0.1'

provided 'org.glassfish:javax.annotation:10.0-b28'

}

apply plugin: 'com.android.application'

apply plugin: 'com.neenbedankt.android-apt'

android {

compileSdkVersion 22

buildToolsVersion '22.0.1'

defaultConfig {

applicationId 'net.rafaeltoledo.tdc2015'

minSdkVersion 15

targetSdkVersion 22

versionCode 1

versionName '1.0'

}

}

dependencies {

compile 'com.android.support:appcompat-v7:22.2.1'

compile 'com.google.dagger:dagger:2.0.1'

apt 'com.google.dagger:dagger-compiler:2.0.1'

provided 'org.glassfish:javax.annotation:10.0-b28' // JSR-330

}

E pronto!

Vamos começar?

@Inject

@Component @Module

@Provides

Dagger 2

@Inject

● Parte da JSR-330

● Marca quais dependências devem ser fornecidas pelo Dagger

● Pode aparecer de 3 formas no código

public class TdcActivityPresenter {

private TdcActivity activity;

private TdcDataStore dataStore;

@Inject

public TdcActivityPresenter(TdcActivity activity,

TdcDataStore dataStore) {

this.activity = activity;

this.dataStore = dataStore;

}

}

1. No Construtor

1. No Construtor

● Todas as dependências vem do grafo de dependências do Dagger

● Anotar uma classe dessa forma faz com que ela também faça parte do grafo de dependências (podendo ser injetada em outras classes)

1. No Construtor

● Limitação: não podemos anotar mais que um construtor com a anotação @Inject

public class TdcActivity extends AppCompatActivity {

@Inject TdcActivityPresenter presenter;

@Inject SharedPreferences preferences;

@Override

protected void onCreate(Bundle bundle) {

super.onCreate(bundle);

getComponent().inject(this);

}

}

2. Nos Atributos da Classe

2. Nos Atributos da Classe

● A injeção nesse caso deve ser manual, caso contrários os atributos serão todos nulos

@Override

protected void onCreate(Bundle bundle) {

super.onCreate(bundle);

getComponent().inject(this);

}

● Limitação: os atributos não podem ser privados!

public class TdcActivityPresenter {

private TdcActivity activity;

@Inject

public TdcActivityPresenter(TdcActivity activity) {

this.activity = activity;

}

@Inject

public void enableAnalytics(AnalyticsManager analytics) {

analytics.track(this);

}

}

3. Em métodos públicos

3. Em métodos públicos

● Utilizado em conjunto com a anotação no construtor da classe

● Para casos onde o objeto injetado necessitamos da instância da própria classe na dependência fornecida

@Inject

public void enableAnalytics(AnalyticsManager analytics) {

analytics.track(this);

}

3. Em métodos públicos

● O método anotado é chamado automaticamente logo após o construtor

@Module

● Parte da API do Dagger

● Utilizada para identificar classes que fornecem dependências

@Module

public class DataModule {

@Provides @Singleton

public OkHttpClient provideOkHttpClient() {

OkHttpClient okHttpClient = new OkHttpClient();

okHttpClient.setConnectionTimeout(10, TimeUnit.SECONDS);

okHttpClient.setReadTimeout(10, TimeUnit.SECONDS);

okHttpClient.setWriteTimeout(10, TimeUnit.SECONDS);

return okHttpClient;

}

@Provides @Singleton

public RestAdapter provideRestAdapter(OkHttpClient client) {

return new RestAdapter.Builder()

.setClient(new OkClient(okHttpClient))

.setEndpoint("https://api.github.com")

.build();

}

}

@Module

public class DataModule {

@Provides @Singleton

public OkHttpClient provideOkHttpClient() {

OkHttpClient okHttpClient = new OkHttpClient();

okHttpClient.setConnectionTimeout(10, TimeUnit.SECONDS);

okHttpClient.setReadTimeout(10, TimeUnit.SECONDS);

okHttpClient.setWriteTimeout(10, TimeUnit.SECONDS);

return okHttpClient;

}

@Provides @Singleton

public RestAdapter provideRestAdapter(OkHttpClient client) {

return new RestAdapter.Builder()

.setClient(new OkClient(okHttpClient))

.setEndpoint("https://api.github.com")

.build();

}

}

@Provide

● Utilizada nas classes anotadas como @Module para identificar quais métodos retornam dependências

@Module

public class DataModule {

@Provides @Singleton

public OkHttpClient provideOkHttpClient() {

OkHttpClient okHttpClient = new OkHttpClient();

okHttpClient.setConnectionTimeout(10, TimeUnit.SECONDS);

okHttpClient.setReadTimeout(10, TimeUnit.SECONDS);

okHttpClient.setWriteTimeout(10, TimeUnit.SECONDS);

return okHttpClient;

}

@Provides @Singleton

public RestAdapter provideRestAdapter(OkHttpClient client) {

return new RestAdapter.Builder()

.setClient(new OkClient(okHttpClient))

.setEndpoint("https://api.github.com")

.build();

}

}

@Module

public class DataModule {

@Provides @Singleton

public OkHttpClient provideOkHttpClient() {

OkHttpClient okHttpClient = new OkHttpClient();

okHttpClient.setConnectionTimeout(10, TimeUnit.SECONDS);

okHttpClient.setReadTimeout(10, TimeUnit.SECONDS);

okHttpClient.setWriteTimeout(10, TimeUnit.SECONDS);

return okHttpClient;

}

@Provides @Singleton

public RestAdapter provideRestAdapter(OkHttpClient client) {

return new RestAdapter.Builder()

.setClient(new OkClient(okHttpClient))

.setEndpoint("https://api.github.com")

.build();

}

}

@Component

● É a anotação que liga os @Modules aos @Injects

● É colocada em uma interface

● Define quais módulos possui, quem será injetado

@Component

● Precisa especificar obrigatoriamente seu escopo (ciclo de vida de suas dependências)

● Pode publicar dependências

● Pode possuir outros componentes

@Singleton

@Component(

modules = {

DataModule.class,

UiModule.class

}

)

public interface TdcAppComponent {

void inject(TdcApplication app);

MainActivity inject(MainActivity activity);

Application getApplication();

AnalyticsManager getAnalyticsManager();

}

@Singleton // Escopo

@Component(

modules = {

DataModule.class,

UiModule.class

}

)

public interface TdcAppComponent {

void inject(TdcApplication app);

MainActivity inject(MainActivity activity);

Application getApplication();

AnalyticsManager getAnalyticsManager();

}

@Singleton

@Component(

modules = {

DataModule.class,

UiModule.class

}

)

public interface TdcAppComponent {

void inject(TdcApplication app);

MainActivity inject(MainActivity activity);

Application getApplication();

AnalyticsManager getAnalyticsManager();

}

@Singleton

@Component(

modules = {

DataModule.class,

UiModule.class

}

)

public interface TdcAppComponent {

void inject(TdcApplication app);

MainActivity inject(MainActivity activity); // Caso queira encadear

Application getApplication();

AnalyticsManager getAnalyticsManager();

}

@Singleton

@Component(

modules = {

DataModule.class,

UiModule.class

}

)

public interface TdcAppComponent {

void inject(TdcApplication app);

MainActivity inject(MainActivity activity);

Application getApplication();

AnalyticsManager getAnalyticsManager(); // Dependências visíveis

} // para outros componentes*

@Singleton

@Component(

modules = {

DataModule.class,

UiModule.class

}

)

public interface TdcAppComponent {

void inject(TdcApplication app);

MainActivity inject(MainActivity activity);

Application getApplication();

AnalyticsManager getAnalyticsManager();

}

@ActivityScope // Escopo personalizado

@Component(

modules = TdcActivityModule.class,

dependencies = TdcComponent.class

)

public interface TdcActivityComponent {

TdcActivity inject(TdcActivity activity);

TdcActivityPresenter presenter();

}

@Generated("dagger.internal.codegen.ComponentProcessor")

public final class DaggerMainComponent implements MainComponent {

private Provider<ApiService> provideApiServiceProvider;

private MembersInjector<MainActivity> mainActivityMembersInjector;

private DaggerMainComponent(Builder builder) {

assert builder != null;

initialize(builder);

}

public static Builder builder() {

return new Builder();

}

public static MainComponent create() {

return builder().build();

}

private void initialize(final Builder builder) {

this.provideApiServiceProvider =

ScopedProvider.create(MainModule_ProvideApiServiceFactory.create(builder.mainModule));

this.mainActivityMembersInjector = MainActivity_MembersInjector.create(

(MembersInjector) MembersInjectors.noOp(), provideApiServiceProvider);

}

@Override

public void inject(MainActivity activity) {

mainActivityMembersInjector.injectMembers(activity);

}

}

class UserController {

UserDao dao;

Logger logger;

Api api;

public UserController() {

dao = new UserDaoImpl();

api = RetrofitApi.getInstance();

logger = Logger.getForClass(UserController.class);

}

void doLogic() {

try {

User user = api.getUser(1);

dao.save(user);

logger.log("Success");

} catch (IOException e) { logger.logException(e); }

}

}

public class TdcApp extends Application {

TdcAppComponent component;

@Override

public void onCreate() {

component = DaggerTdcAppComponent.create();

component = DaggerTdcAppComponent.builder()

.dataModule(new DataModule(this)) // Módulo com construtor

.build(); // parametrizado

}

}

Instanciando um Componente

Dagger 2Escopos dinâmicos

Escopo = ciclo de vida do grafo de dependências (componente)

@Singleton

@Component(

modules = {

DataModule.class,

UiModule.class

}

)

public interface TdcAppComponent {

void inject(TdcApplication app);

MainActivity inject(MainActivity activity);

Application getApplication();

AnalyticsManager getAnalyticsManager();

}

@ActivityScope // Escopo personalizado

@Component(

modules = TdcActivityModule.class,

dependencies = TdcComponent.class

)

public interface TdcActivityComponent {

TdcActivity inject(TdcActivity activity);

TdcActivityPresenter presenter();

}

@Scope

@Retention(RetentionPolicy.RUNTIME)

public @interface UserScope {

}

Definição de um escopo

Exemplo de uso

Miroslaw Stanek frogermcs.github.io

@Singleton

@Component(modules = DataModule.class)

public interface TdcAppComponent {

UserComponent plus(UserModule module);

// não é necessário expor as dependências

}

Implementação

@UserScope

@Subcomponent(modules = UserModule.class)

public interface UserComponent {

UserActivityComponent plus(UserActivityModule module);

}

Implementação

public class TdcApp extends Application {

TdcAppComponent component;

UserComponent userComponent;

public UserComponent createUserComponent(User user) {

userComponent = component.plus(new UserModule(user));

return userComponent;

}

public void releaseUserComponent() {

userComponent = null;

}

...

}

Implementação

Dagger 2Outras coisas legais!

@Inject

Lazy<SharedPreferences> prefs;

void salvar() {

prefs.get().edit().putString("status", "ok!").apply();

}

Dependências “Preguiçosas”

// No módulo

@Provides @Singleton

@ApiUrl String provideApiUrl(Context context) {

return context.getString(R.string.api_url);

}

@Provides @Singleton

@AccessToken String provideRestAdapter(SharedPreferences prefs) {

return prefs.getString("token", null);

}

// Na classe a ser injetada

@Inject @AccessToken

String accessToken;

Qualificadores

// Declarando sua anotação

@MapKey(unwrapValue = true)

@interface GroupKey {

String value();

}

@MapKey

// Fornecendo dependências no módulo

@Provides(type = Type.MAP)

@GroupKey("um")

String provideFirstValue() {

return "primeiro valor";

}

@Provides(type = Type.MAP)

@GroupKey("dois")

String provideSecondValue() {

return "segundo valor";

}

@MapKey

// Uso

@Inject

Map<String, String> map; // {um=primeiro valor, dois=segundo valor}

● Por enquanto, só aceita Map e Set, e valores do tipo String e Enumeradores

@MapKey

Ok! Tudo muito bonito mas...

O que eu ganho de fato com tudo isso?

Grandes benefícios

● Código mais limpo e organizado

● Melhorias no gerenciamento de objetos (melhor controle de singletons)

● Código mais fácil de TESTAR

Dagger 2 e Testes

● Programar orientado a interfaces - mocks

● Sobrescrita de Componentes e Módulos

Dagger 2 e Testes

@Module

public class TestMainModule {

@Provides // Poderíamos fazer com o Retrofit Mock!

public ApiService provideApiService() {

return new ApiService() {

@Override

public List<Repo> listRepos() {

return Arrays.asList(

new Repo("my-test-repo"),

new Repo("another-test-repo")

);

}

};

}

}

Dagger 2 e Testes

@Component(modules = DebugMainModule.class)

public interface TestMainComponent extends MainComponent {

}

Dagger 2 e Espresso 2

@RunWith(AndroidJUnit4.class)

public class MainActivityTest {

// ...

@Before

public void setUp() {

Instrumentation instrumentation =

InstrumentationRegistry.getInstrumentation();

MainApplication app = (MainApplication)

instrumentation.getTargetContext().getApplicationContext();

app.setComponent(DaggerDebugMainComponent.create());

rule.launchActivity(new Intent());

}

}

Dagger 2 e Robolectric 3

@RunWith(RobolectricGradleTestRunner.class)

@Config(sdk = 21, constants = BuildConfig.class)

public class MainActivityTest {

// ...

@Before

public void setUp() {

MainComponent component = DaggerDebugMainComponent.create();

MainApplication app =

((MainApplication) RuntimeEnvironment.application)

.setComponent(component);

}

}

Dagger 2github.com/rafaeltoledo/dagger2-tdc2015

Para saber mais

● Site oficial - google.github.io/dagger

● Dagger 2 - A New Type of Dependency Injection (Gregory Kick) - vai.la/fdwt

● The Future of Dependency Injection with Dagger 2 (Jake Wharton) - vai.la/fdwy

● froger_mcs Dev Blog - frogermcs.github.io

OBRIGADO!

rafaeltoledo.net

@_rafaeltoledo

Desenv. Android @ Concrete Solutionsestamos contratando!

concretesolutions.com.br/carreira