вторник, 22 августа 2017 г.

Пример работы с Dagger 2 в связке с Retrofit



Написание кода меня очень увлекает и я люблю это, люблю писать всякие ништячки — программки, утилитки и т.д. Мне это доставляет удовольствие. Но иногда меня очень сильно бесит большое количество объявлений новых объектов типа Object object = new Object();где Object это какой-то класс объект в котором хранятся данные или еще что-то на подобии.

Как и ButterKnife, я стараюсь везде пихать и Dagger 2 который умеет красиво облегчать код и по красоте убирать лишние объявления создания новых объектов каждый раз когда нам нужно их создавать. 

По этой библиотеке есть очень много статей на хабре — раздватричетырепять. По ней есть куча статей в интернете в общем — раздватри, но я приведу пример того как я вижу и как мне проще подключать эту библиотеку, возможно моя статья окажется лучше чем статьи те которые я нашел в интернете, так как я сходу не смог понять что да как делают в них в свое время. 


Основную базовую информацию что это, и как это можно почерпнуть из ссылок выше, там в красках описывается что такое Dagger 2, зачем он нужен, для чего его используют, кто его используют и как, там есть куча примеров как люди изгаляются и делают свой код лучше и сложнее с точки зрения новичка. Я же расскажу то как я использую его и это как по мне достаточно для того что бы заинтересовать использовать эту библиотеку. Она как минимум сокращает код, и добавляет удобность использования нужных объектов в программе. Но иногда Dagger 2 умеет выдавать кучу ошибок из-за какой-то случайно добавленной аннотации, а ошибок оно показывает такое ощущение что проект вообще написан одним местом… Но со временем привыкаешь к этому и уже не обращаешь внимания, так как по сути все проблемы в этих ошибках описываются и их очень легко решить, тем более они повторяются.

Приложение мы будем делать такое как мы уже делали в статье «Создаем бесконечный список с помощью RecyclerView», по сути у нас тут будет код из этой статьи, почти, обернутый в обертку Dagger'a без пагинации что бы небыло лишнего кода, а то его и так дохрена из-за ретрофита будет.

В общем для начала нам нужно подключить библиотеки для работы с Retrofit, OkHttp, RecyclerView, Dagger 2 и ButterKnife. В общем в этом проекте у нас будет дохера либ которые мы будем использовать, но тут мы уже вроде как опытные и использовали эти библиотеки все, по этому можем себе это позволить.

Вот так будет выглядеть gradle файл с зависимостями.

app/build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.project.dajver.dagger2testexample"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.3.1'

    compile 'com.google.dagger:dagger:2.11'
    compile 'com.google.dagger:dagger-android:2.11'
    annotationProcessor 'com.google.dagger:dagger-android-processor:2.11'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.11'

    compile 'com.android.support:recyclerview-v7:25.3.1'

    compile 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

    compile 'com.squareup.retrofit2:retrofit:2.0.1'
    compile 'com.squareup.retrofit2:converter-gson:2.0.1'
    compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
    compile 'com.squareup.retrofit2:converter-scalars:2.0.1'
}

Вот тут мы подключили кучу в общем библиотек я думаю для всех кто читает мои статьи это знакомый набор кроме даггера, и картина особо знакомая так как без подключения библиотек ни один проект не начнется :), я лично с этого всегда начинаю.

Дальше нам нужно добавить пермишен для доступа в интернет в AndroidManifest.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.project.dajver.dagger2testexample">

    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        android:name=".App"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name=".SecondActivity" />
    </application>

</manifest>

Тут мы добавили пермишен для доступа в интернет и добавили файл App который у нас является оберткой для взаимодействия даггера с жизненным циклом приложения. Этот файл мы создадим чуть позже когда у нас будет компонент и модуль для синхронизации зависимостей.

С первой частью мы справились, у нас подключились все нужные библиотеки и подтянулись все нужные файлы. Дальше нам нужно начать создавать магию.

Мы должны создать интерфейс с аннотацией @Compoent, в котором у нас будет объявленны активити которые мы будем использовать для взаимодействия, ну то есть активити в которых мы будем использовать Dagger 2, и будет объявление собственно модуля управляющего инъекциями в проекте.

AppComponent.java
import com.project.dajver.dagger2testexample.App;
import com.project.dajver.dagger2testexample.MainActivity;
import com.project.dajver.dagger2testexample.SecondActivity;
import com.project.dajver.dagger2testexample.modules.AppModule;

import javax.inject.Singleton;

import dagger.Component;
import dagger.android.AndroidInjector;

@Singleton
@Component(modules = { AppModule.class })
public interface AppComponent extends AndroidInjector<App> {

    void inject(MainActivity mainActivity);
    void inject(SecondActivity secondActivity);

    final class Initializer {
        private Initializer() { }

        public static AppComponent init(App app) {
            return DaggerAppComponent.builder()
                    .appModule(new AppModule(app))
                    .build();
        }
    }
}

Вот тут видно как мы создали интерфейс, ему мы подключили модуль который мы создадим дальше, он нам нужен для управления инъекций и хранения данных. Внутри интерфейса мы добавили в инъекции две активити которые у нас будут использова Dagger. 
Еще ниже мы создали класс Initializer который нам нужен для инициализации синглтона на весь жизненный цикл программы для Dagger'a, и в методе init проинициализировали наш модуль. Хочу заметить что у нас над @Component есть аннотация @Singlton — он нам нужен для того что бы Dagger понял что этот интерфейс нам не пересоздавать каждый раз когда мы переходим между активити, ну в общем вы поняли, типичное поведение для синглтона — раз и на всю жизнь.

AppModule.java
import android.app.Application;

import com.project.dajver.dagger2testexample.App;
import com.project.dajver.dagger2testexample.api.model.imp.FetchedDataPresenterImpl;

import javax.inject.Singleton;

import dagger.Module;
import dagger.Provides;

@Module
public class AppModule {

    private App app;

    public AppModule(App application) {
        app = application;
    }

    @Provides
    @Singleton
    protected Application provideApplication() {
        return app;
    }

    @Provides
    @Singleton
    protected FetchedDataPresenterImpl provideFetchedData() {
        return new FetchedDataPresenterImpl();
    }
}

Этот класс нам нужен для сохранение инстанса данных, для сохранения context'a в приложении что бы мы не теряли данные при переходе между экранами и т.д. Хочу заметить аннотацию @Module — она указывает на то что этот класс мы будем использовать как модуль в котором будет храниться вся хрень касательно нашего приложения.

В начале класса мы создали конструктор с инстансом класса App, мы его передает для сохранения контекста. Дальше мы создаем метод provideApplication() который собственно хранит наш context в приложении, и не пересоздает его каждый раз при переходах. А еще ниже у нас метод provideFetchedData() который хранит инстанс нашего класса с данными который мы создадим позже, в этом классе у нас будут хранитья данные с запроса к GitHub API.

App.java
import android.app.Application;

import com.project.dajver.dagger2testexample.components.AppComponent;

public class App extends Application {

    private static AppComponent appComponent;
    private static App app;

    @Override
    public void onCreate() {
        super.onCreate();
        app = this;
        buildComponentGraph();
    }

    public static AppComponent component() {
        return appComponent;
    }

    public static void buildComponentGraph() {
        appComponent = AppComponent.Initializer.init(app);
    }
}

В этом классе мы объеденяем наш класс компонент и наш модуль, по сути тут мы создаем объект нашего модуля в компоненте с интансом контекста из Application класса.

В общем что мы тут видим. В onCreate() мы создали инстанс нашего Application класса, и запускаем метод buildComponentGraph() который создает наш AppComponent. Это мы делаем для того что бы дальше в наших Activity могли писать вот такую хрень
App.component().inject(this);
, для того что бы Dagger понимал что в этом классе у нас будут работать инъекции и доступ к сохраненным данным в синглтоне. Дальше желательно сбилдить проект что бы Dagger создал все нужные компоненты и классы для связи всего этого в одну кучу.

Теперь когда у нас есть вот эта конструкция мы можем начать писать смело везде где мы хотим аннотацию @Inject и у нас будет создаваться нужные нам объекты классов без каких либо дополнительных new Object().

API.java
import com.project.dajver.dagger2testexample.api.model.GitHubModel;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;

public interface API {

    @GET("search/repositories")
    Call<GitHubModel> getSearchedRepos(@Query("q") String q,
                                       @Query("page") int page,
                                       @Query("per_page") int perPage);
}

В общем создали мы значит интерфейс, стандартный для работы с Retrofit, в нем прописали адрес куда будем стучаться и параметры которые будем слать. Подетальней про работу с ретрофит можно почитать тут, я особо расписывать детали не буду ибо делал это милион раз уже.

Так же у нас тут не хватает пары классов которые будут парсить респонс с сервера.

GitHubModel.java
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

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

public class GitHubModel {
    @SerializedName("items")
    @Expose
    private List<GitHubItemModel> items = new ArrayList<>();

    public List<GitHubItemModel> getItems() {
        return items;
    }

    public void setItems(List<GitHubItemModel> items) {
        this.items = items;
    }
}

GitHubItemModel.java
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

public class GitHubItemModel {
    @SerializedName("id")
    @Expose
    private Integer id;
    @SerializedName("name")
    @Expose
    private String name;
    @SerializedName("description")
    @Expose
    private String description;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

Возвращать эти два класса нам будут список в котором будет айди, описание и название репозиториия. Этого нам хватит для примера, на деле конечно это API возвращает кучу инфы о репозитории. Дальше создадим клиент для доступа к запросам.

RestClient.java
import javax.inject.Inject;

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;

public class RestClient {

    public static final String BASE_URL = "https://api.github.com/";

    private final API service;

    @Inject
    public RestClient() {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(ScalarsConverterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        service = retrofit.create(API.class);
    }

    public API getService() {
        return service;
    }
}

Тут мы указали в BASE_URL ссылку на базовый адрес куда мы будем обращаться для получения данных с сервера. В конструкторе проинициализировали Retrofit и наш интерфейс с параметрами, и дальше ниже создали метод getService() который возвращает нам наш сервис.

Еще нам нужно создать два класса. Один — интерфейс который будет иметь три метода, а второй класс будет имплементацией этого интерфейса, в нем мы будем описывать работу этих методов.

IFetchData.java
import com.project.dajver.dagger2testexample.api.model.GitHubItemModel;
import com.project.dajver.dagger2testexample.api.model.GitHubModel;

import java.util.List;

public interface IFetchedData {

    void setGitHubData(GitHubModel data);
    List<GitHubItemModel> getAllData();
    GitHubItemModel getGitHubData(int position);
}

Первый метод у нас будет сетить данные в GitHubModel, а остальные два будут возвращать нам или все данные или одну пачку по позиции из списка. Это у нас чисто интерфейс, дальше последует имплементация его и описание их функций.

Для того что бы мы могли использовать @Inject в наших активити, нам нужно всегда прописывать над конструктором класса который мы хотим использовать аннотацию @Inject, что бы Dagger понимал что этот класс заинъекчен уже и что бы он мог его использовать. 

FetchedDataPresenterImpl.java
import com.project.dajver.dagger2testexample.api.model.GitHubItemModel;
import com.project.dajver.dagger2testexample.api.model.GitHubModel;

import java.util.List;

import javax.inject.Inject;

public class FetchedDataPresenterImpl implements IFetchedData {

    private GitHubModel gitHubModel;

    @Inject
    public FetchedDataPresenterImpl() { }

    @Override
    public void setGitHubData(GitHubModel data) {
        this.gitHubModel = data;
    }

    @Override
    public List<GitHubItemModel> getAllData() {
        return gitHubModel.getItems();
    }

    @Override
    public GitHubItemModel getGitHubData(int position) {
        return gitHubModel.getItems().get(position);
    }
}

Как говорил выше, для того что бы мы могли заинъектить класс в активити нам нужно в конструкторе класса всегда указывать аннотацию @Inject. Ну и как я говорил выше, этот класс имплементация по этому тут мы описываем запись данных в GitHubModel и потом выем этих данных из него же по средством созданных из интерфейса методов.

Дальше нам нужно создать адаптер для работы с RecyclerView, в него засетим данные с нашего респонса и отобразим красоту.

RecycleListAdapter.java
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.project.dajver.dagger2testexample.R;
import com.project.dajver.dagger2testexample.api.model.GitHubItemModel;

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

import javax.inject.Inject;

import butterknife.BindView;
import butterknife.ButterKnife;

public class RecycleListAdapter extends RecyclerView.Adapter<RecycleListAdapter.ViewHolder>{

    private List<GitHubItemModel> searchModels = new ArrayList<>();
    private OnItemClickListener onItemClickListener;

    @Inject
    public RecycleListAdapter() { }

    public void addAll(List<GitHubItemModel> searchModels) {
        this.searchModels = searchModels;
    }

    @Override
    public RecycleListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_music, parent, false);
        RecycleListAdapter.ViewHolder pvh = new RecycleListAdapter.ViewHolder(v);
        return pvh;
    }

    @Override
    public void onBindViewHolder(final RecycleListAdapter.ViewHolder holder, final int position) {
        holder.title.setText(searchModels.get(position).getName());
    }

    @Override
    public int getItemCount() {
        return searchModels.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {

        @BindView(R.id.textView)
        TextView title;

        ViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                   onItemClickListener.onItemClick(getAdapterPosition());
                }
            });
        }
    }

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    public interface OnItemClickListener {
        void onItemClick(int position);
    }
}

Вот такой адаптер, если в кратце то в методе addAll() мы добавляем данные в адаптер. В методе onCreateViewHolder() мы проинициализировали леяут с которым будем работать. В onBindViewHolder() мы засетапили данные из списка в текствью. Создали вью холдер и интерфейс который по клику кидает колбек в активити. Как-то так.

В активити мы все это объеденим до кучи и получим рабочий код. Надеюсь до теперешного момента все понятно…

MainActivity.java
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

import com.project.dajver.dagger2testexample.adapter.RecycleListAdapter;
import com.project.dajver.dagger2testexample.api.RestClient;
import com.project.dajver.dagger2testexample.api.model.GitHubModel;
import com.project.dajver.dagger2testexample.api.model.imp.FetchedDataPresenterImpl;

import javax.inject.Inject;

import butterknife.BindView;
import butterknife.ButterKnife;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

import static com.project.dajver.dagger2testexample.SecondActivity.EXTRA_POSITION;

public class MainActivity extends AppCompatActivity implements Callback<GitHubModel>,
        RecycleListAdapter.OnItemClickListener {

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

    @Inject
    RecycleListAdapter recycleListAdapter;
    @Inject
    RestClient restClient;
    @Inject
    FetchedDataPresenterImpl fetchedData;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ButterKnife.bind(this);
        App.component().inject(this);

        recycleViewSetup(recyclerView);

        restClient.getService().getSearchedRepos("retrofit", 0, 100).enqueue(this);
    }

    public void recycleViewSetup(RecyclerView recyclerView) {
        recyclerView.setHasFixedSize(true);
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(linearLayoutManager);
        recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
    }

    @Override
    public void onResponse(Call<GitHubModel> call, Response<GitHubModel> response) {
        GitHubModel githubModel = response.body() != null ? response.body() : new GitHubModel();
        fetchedData.setGitHubData(githubModel);

        recycleListAdapter.addAll(fetchedData.getAllData());
        recycleListAdapter.setOnItemClickListener(this);
        recyclerView.setAdapter(recycleListAdapter);
    }

    @Override
    public void onFailure(Call<GitHubModel> call, Throwable t) {
        t.printStackTrace();
    }

    @Override
    public void onItemClick(int position) {
        Intent intent = new Intent(MainActivity.this, SecondActivity.class);
        intent.putExtra(EXTRA_POSITION, position);
        startActivity(intent);
    }
}

В этой активити мы заинъектили все наши нужные классы, теперь их инстанс создастся и не надо ничего создавать дополнительно, просто добавить аннотацию @Inject к названию класса.
Дальше в onCreate() мы прописали что мы будем использовать в этой активити ButterKnife и Dagger с помощью вызова ниже который мы создали в классе App. Так же мы тут вызываем recycleViewSetup() в который передаем RecyclerView что бы стилизовать дизайн этого списка. И делаем запрос на сервер.
Ниже у нас колбек ретрофита onResponse() который возвращает нам GitHubModel который мы передаем дальше в FetchedDataPresenterImpl, что бы потом можно было его использовать в других классах, и потом сетим эти данные в адаптер.
onFailure() у нас выводит ошибку в лог.
onItemClick() — по клику на айтем у нас идет переход на следующую активити, в которой мы будем выводить детали этого айтема по позиции в списке.

SecondActivity.java
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;

import com.project.dajver.dagger2testexample.api.model.imp.FetchedDataPresenterImpl;

import javax.inject.Inject;

import butterknife.BindView;
import butterknife.ButterKnife;

public class SecondActivity extends AppCompatActivity {

    public static final String EXTRA_POSITION = "position";

    @BindView(R.id.text)
    TextView text;

    @Inject
    FetchedDataPresenterImpl fetchedData;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        ButterKnife.bind(this);
        App.component().inject(this);

        int position = getIntent().getIntExtra(EXTRA_POSITION, 0);
        text.setText("Name: " + fetchedData.getGitHubData(position).getName() +
                "\nDescription: " + fetchedData.getGitHubData(position).getDescription());
    }
}

Ну а во втором классе мы принимаем позицию и по ней с помощью этой позиции мы достаем из FetchedDataPresenterImpl нужные нам данные. Тут как и в предыдущей активити мы прописали что будем использовать ButterKnife и Dagger который у нас проинициализирован в классе App. В общем вы должны запомнить что везде где хотите использовать Dagger вам нужно прописывать зависимость от него в виде
App.component().inject(this);
и в AppComponent добпалят инъекции к нужным экранам.


Исходники:

GitHub

2 комментария: