Статьи

Применение архитектуры Google Android с базой данных ObjectBox

Если вы еще не видели архитектуру Google, вы можете узнать больше об этом здесь . Кроме того, если вы не знакомы с ObjectBox, проверьте этот пост .

Вступление

Цель архитектуры — в итоге получить что-то вроде этого:

Основное отличие состоит в том, что я буду использовать ObjectBox вместо Room. Архитектура не требует каких-либо конкретных реализаций. Вы всегда можете поменять детали реализации. Я обнаружил, что ObjectBox — одна из самых простых баз данных, и она позволяет выполнять реактивные запросы независимо от RxJava (хотя вы можете использовать RxJava, если хотите).

Чтобы увидеть полный пример, вы можете найти репозиторий GitHub здесь .

Начиная

Для начала я добавил две модели в свой проект: Zoo и Animal. Зоопарк имеет отношение один-ко-многим с животными. Вы можете увидеть их реализации ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Entity
public class Zoo {
 
    @Id
    private long id;
    private String name;
 
    @Backlink
    public ToMany<Animal> animals;
 
    public Zoo() {
    }
 
    public Zoo(String name) {
        this.name = name;
    }
 
    public long getId() {
        return id;
    }
 
    public void setId(long id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Entity
public class Animal {
 
    @Id
    private long id;
 
    private String name;
    private String image;
    private String group;
 
    public ToOne<Zoo> zoo;
 
    public Animal() {
    }
 
    public Animal(String name, String image, String group) {
        this.name = name;
        this.image = image;
        this.group = group;
    }
 
    public long getId() {
        return id;
    }
 
    public void setId(long id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getImage() {
        return image;
    }
 
    public void setImage(String image) {
        this.image = image;
    }
 
    public String getGroup() {
        return group;
    }
 
    public void setGroup(String group) {
        this.group = group;
    }
}

Затем я создал простой MainActivity (слой View) с RecyclerView для отображения списка зоопарков и FloatingActionButton (FAB) для добавления новых зоопарков. Я также добавил некоторые фиктивные данные для этого примера, чтобы в приложении были данные для отображения. Код активности ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class MainActivity extends AppCompatActivity {
 
    private ZooListViewModel mViewModel;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        RecyclerView recyclerView = findViewById(R.id.activity_main_recyclerview);
        recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
        ZooAdapter adapter = new ZooAdapter();
        adapter.setOnClickListener((view, position) -> {
            Intent zooIntent = new Intent(MainActivity.this, ZooActivity.class);
            zooIntent.putExtra(ZooActivity.EXTRA_ZOO_ID, adapter.getItemId(position));
            startActivity(zooIntent);
        });
        recyclerView.setAdapter(adapter);
 
        mViewModel = ViewModelProviders.of(this).get(ZooListViewModel.class);
        mViewModel.getZoos().observe(this, (adapter::update));
 
        findViewById(R.id.activity_main_fab).setOnClickListener(v -> {
            DialogFragment zooFragment = ZooFragment.newInstance();
            zooFragment.show(getSupportFragmentManager(), ZooFragment.class.getName());
        });
    }
 
    @Override
    protected void onResume() {
        super.onResume();
        mViewModel.refreshZoos();
    }
}

Я создаю ViewModel для MainActivity, который отвечает за загрузку списка зоопарков. Действие наблюдает за этими данными, и когда они изменяются, это сообщает ZooAdapter об этом. Я пропустил детали адаптера и макетов, так как они являются обычными реализациями. Вы можете просмотреть адаптер здесь, если хотите его увидеть.

Вы можете увидеть результат ниже:

ViewModel

ViewModel по сути просто отвечает за связь между хранилищем и представлением. Он отправляет запросы из представления в хранилище и также возвращает результаты в представление. Я пытаюсь создать одну ViewModel для каждого вида, но вы можете использовать больше, если это имеет смысл. Все мои ViewModels происходят из класса BaseViewModel, который управляет подписками ObjectBox:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class BaseViewModel extends ViewModel {
 
    private final List<DataSubscription> mSubscriptions;
 
    @Override
    protected void onCleared() {
        super.onCleared();
        for (DataSubscription subscription : mSubscriptions) {
            if (!subscription.isCanceled()) {
                subscription.cancel();
            }
        }
    }
 
    protected final void addSubscription(@NonNull DataSubscription subscription) {
        mSubscriptions.add(subscription);
    }
 
    public BaseViewModel() {
        mSubscriptions = new ArrayList<>();
    }
}

Чтобы действительно следовать этому типу архитектуры должным образом, я не должен подвергать ObjectBox слою ViewModel. Я оставил это так, чтобы пример был проще, но вы можете абстрагировать его с помощью чего-то вроде RxRelay .

ViewModel, который управляет моей MainActivity, выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ZooListViewModel extends BaseViewModel {
 
    private MutableLiveData<List<Zoo>> mZoosLiveData;
 
    public ZooListViewModel() {
        mZoosLiveData = new MutableLiveData<>();
        DataSubscription subscription = ZooRepository.subscribeToZooList(this::refreshZoos);
        addSubscription(subscription);
    }
 
    private void refreshZoos(List<Zoo> zoos) {
        mZoosLiveData.postValue(zoos);
    }
 
    public LiveData<List<Zoo>> getZoos() {
        return mZoosLiveData;
    }
 
    public void refreshZoos() {
        ZooRepository.refreshZoos();
    }
}

Он инициализируется с помощью MutableLiveData для наблюдения за List

и ObjectBox DataSubscription. Эта подписка отслеживает изменения в базе данных, она устанавливает MutableLiveData при каждом изменении данных. Который затем уведомит своих наблюдателей об изменениях. Приятной особенностью LiveData является то, что его наблюдатели осведомлены о жизненном цикле, поэтому фоновые обновления данных будут отправляться только при активном представлении.

Он также предоставляет метод для обновления зоопарков. Это приведет к тому, что репозиторий обновит данные из источника. Обычно это удаленный сервер, поэтому он отправляет сетевой запрос для получения последних данных. В результате база данных ObjectBox будет обновлена, и соответствующие подписки будут уведомлены. В результате LiveData в моей ViewModel обновляется, передает результат своим наблюдателям и уведомляет слой представления.

вместилище

Хранилище отвечает за передачу запросов от ViewModel в базу данных и сеть и за возврат ответа. Я разбил эти слои на классы API (для доступа к удаленным API) и классы DAO (для доступа к базе данных). Вы можете увидеть мою реализацию здесь:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class ZooRepository {
 
    public static DataSubscription subscribeToZooList(DataObserver<List<Zoo>> observer) {
        return ZooDAO.subscribeToZooList(observer);
    }
 
    public static DataSubscription subscribeToZoo(DataObserver<Zoo> observer, long id, boolean singleUpdate) {
        return ZooDAO.subscribeToZoo(observer, id, singleUpdate);
    }
 
    public static void refreshZoo(long id) {
        ZooAPI.loadZoo(id, zooResponse -> {
            if (zooResponse != null && zooResponse.getStatus() == Response.STATUS_SUCCESS) {
                ZooParser parser = new ZooParser(zooResponse.getPayload());
                parser.parseZoo();
                Zoo zoo = parser.getZoo();
                if (zoo != null) {
                    ZooDAO.insertZoo(zoo);
                }
            }
        });
    }
 
    public static void refreshZoos() {
        ZooAPI.loadZoos(zoosResponse -> {
            if (zoosResponse != null && zoosResponse.getStatus() == Response.STATUS_SUCCESS) {
                ZooParser parser = new ZooParser(zoosResponse.getPayload());
                parser.parseZooList();
                List<Zoo> zoos = parser.getZooList();
                if (zoos != null) {
                    ZooDAO.insertZoos(zoos);
                }
            }
        });
    }
 
    public static void addZoo(Zoo newZoo, MutableLiveData<ZooUpdateResponse> liveResponse) {
        liveResponse.postValue(new ZooUpdateResponse(Response.STATUS_LOADING));
        ZooAPI.addZoo(newZoo, zooResponse -> handleZooResponse(zooResponse, liveResponse));
    }
 
    public static void updateZoo(Zoo zoo, MutableLiveData<ZooUpdateResponse> liveResponse) {
        liveResponse.postValue(new ZooUpdateResponse(Response.STATUS_LOADING));
        ZooAPI.updateZoo(zoo, zooResponse -> handleZooResponse(zooResponse, liveResponse));
    }
 
    private static void handleZooResponse(Response zooResponse, MutableLiveData<ZooUpdateResponse> liveResponse) {
        if (zooResponse != null) {
            if (zooResponse.getStatus() == Response.STATUS_SUCCESS) {
                ZooParser parser = new ZooParser(zooResponse.getPayload());
                parser.parseZoo();
                Zoo zoo = parser.getZoo();
                if (zoo != null) {
                    ZooDAO.insertZoo(zoo);
                }
            }
 
            liveResponse.postValue(new ZooUpdateResponse(zooResponse.getStatus()));
        }
    }
}

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

Если хранилищу присваивается Observer или LiveData, при завершении он устанавливает свое значение, чтобы передать ответ обратно в ViewModel. Эти объекты ответа могут содержать дополнительную информацию, такую ​​как коды состояния (успех, сбой и т. Д.) И сообщения об ошибках. Поскольку модели данных не будут содержать этот тип информации. Реализация этих объектов ответа действительно зависит от того, какая информация нужна ViewModel.

анализ

Чтобы хранилище не распухло, я разбил логику разбора ответа на его собственный набор классов. Эти классы синтаксического анализа просто принимают строку ответа (вероятно, JSON) и преобразуют ее в соответствующую модель (ы). Для анализа ответов Zoo мой класс выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ZooParser {
 
    private String mResponse;
    private Zoo mZoo;
    private List<Zoo> mZooList;
    private List<Animal> mAnimalList;
    private Gson mGson;
 
    public ZooParser(String response) {
        mResponse = response;
        mGson = new Gson();
    }
 
    public void parseZooList() {
        if (mResponse != null) {
            Zoo[] zoos = mGson.fromJson(mResponse, Zoo[].class);
            mZooList = Arrays.asList(zoos);
        }
    }
 
    public void parseZoo() {
        if (mResponse != null) {
            mZoo = mGson.fromJson(mResponse, Zoo.class);
        }
    }
 
    public Zoo getZoo() {
        return mZoo;
    }
 
    public List<Zoo> getZooList() {
        return mZooList;
    }
 
    public List<Animal> getAnimalList() {
        return mAnimalList;
    }
}

Вы можете заметить, что я включил список животных в анализатор — это потому, что вполне возможно, что ответ для зоопарка может содержать животных. Хотя на самом деле я не реализовывал такие ответы в этом примере. Идея в том, что класс Parser не отображает объекты JSON напрямую в мои модели данных. Он анализирует весь ответ, который может включать несколько моделей данных или конкретные ключи, которые не принадлежат моделям (например, количество страниц).

DAO

Этот слой отвечает за взаимодействие с базой данных. Я бы создал классы DAO для каждой модели, например, мой ZooDAO:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class ZooDAO {
 
    private static Box<Zoo> getZooBox() {
        BoxStore boxStore = App.getBoxStore();
        return boxStore.boxFor(Zoo.class);
    }
 
    public static DataSubscription subscribeToZooList(DataObserver<List<Zoo>> observer) {
        return getZooBox().query().build().subscribe().on(AndroidScheduler.mainThread()).observer(observer);
    }
 
    public static DataSubscription subscribeToZoo(DataObserver<Zoo> observer, long id, boolean singleUpdate) {
        SubscriptionBuilder<Zoo> builder = getZooBox().query().eager(Zoo_.animals).equal(Zoo_.id, id).build().subscribe().transform(list -> {
            if (list.size() == 0) {
                return null;
            } else {
                return list.get(0);
            }
        }).on(AndroidScheduler.mainThread());
 
        if (singleUpdate) {
            builder.single();
        }
        return builder.observer(observer);
    }
 
    public static void insertZoo(Zoo zoo) {
        getZooBox().put(zoo);
    }
 
    public static void insertZoos(Collection<Zoo> zoos) {
        getZooBox().put(zoos);
    }
}

Здесь вы можете увидеть доступ к ObjectBox напрямую. Он выполняет любые необходимые операции с базой данных и создает подписки на запросы. Эти запросы можно наблюдать в ViewModel, которая уведомляет LiveData о любых обновлениях данных.

API

Уровень API используется для отправки запросов в сеть и передачи этих ответов обратно в хранилище. Реализация в этом примере является макетом, вы можете увидеть источник здесь . Это может использовать любой тип сетевых запросов, например, вы можете использовать Retrofit .

Репозиторий наблюдает за ответом API и обрабатывает их. Каждый запрос API будет создавать объект Response:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Response {
 
    public static final int STATUS_LOADING = 0, STATUS_SUCCESS = 1, STATUS_FAIL = 2;
 
    @Retention(SOURCE)
    @IntDef({STATUS_LOADING, STATUS_SUCCESS, STATUS_FAIL})
    @interface Status {
    }
 
    private final int mStatus;
    private String mPayload;
 
    public Response(@Status int status, String payload) {
        mStatus = status;
        mPayload = payload;
    }
 
    @Status
    public int getStatus() {
        return mStatus;
    }
 
    public String getPayload() {
        return mPayload;
    }
}

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

Собираем все вместе

В этом примере у меня есть MainActivity, наблюдающая за данными LiveData в зоопарках. Он будет уведомлен всякий раз, когда это значение установлено. Например, если я добавлю новый зоопарк, он автоматически обновит вид. Это потому, что я настроил DataSubscription с ObjectBox для наблюдения за изменениями. Вы можете увидеть это в действии здесь:

Вы можете найти исходный код этого DialogFragment здесь . Важной частью этого класса является то, что он наблюдает ответ здесь:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
...
 
        ZooFragmentViewModel viewModel = ViewModelProviders.of(this).get(ZooFragmentViewModel.class);
        viewModel.getZooUpdateResponse().observe(this, response -> {
            if (response != null) {
                switch (response.getStatus()) {
                    case Response.STATUS_LOADING:
                        showProgressBar(true);
                        break;
                    case Response.STATUS_SUCCESS:
                        dismiss();
                        break;
                    case Response.STATUS_FAIL:
                        showProgressBar(false);
                        Toast.makeText(getContext(), response.getErrorMessage(), Toast.LENGTH_SHORT).show();
                        break;
                }
            }
        });
 
...

Когда действие сохранения запускается в ViewModel, он устанавливает ответ LiveData со статусом загрузки. Затем, когда это удается или не удается, он также возвращает этот результат. Как только он вернется к MainActivity, вы увидите, что новый зоопарк был добавлен без явного запуска обновления.

Заворачивать

Чтобы увидеть полный пример, вы можете найти репозиторий GitHub здесь . Но вы можете применить то, что я объяснил здесь, к любым новым представлениям / моделям / репозиториям.

Я считаю, что эту архитектуру немного легче понять и применить, чем полностью Чистую Архитектуру . Но применяются те же принципы — как выделение уникальных слоев и возложение на них единой ответственности. Я не считаю, что то, что я представил здесь, является полным, оно должно быть отправной точкой. Вы можете использовать его как есть, но вы должны подумать о том, как вы можете создать и улучшить его. Движение к чему-то похожему на Boilerplate от Buffer’s Clean Architecture является целью. Но, может быть, трудно понять все эти концепции без твердой основы, на которой можно было бы стоять.

Кроме того, чтобы облегчить понимание, я старался свести зависимости к минимуму. Я не включал такие вещи, как Retrofit, OkHttp, Butterknife, RxJava, Dagger и т. Д. Вы можете добавлять любые библиотеки, которые вам нравятся, вы также можете менять ObjectBox. Хотя я думаю, что это хорошо работает с этим подходом.

Дайте мне знать, что вы думаете об этом подходе. Также, если у вас есть какие-либо комментарии, вопросы или предложения, пожалуйста, оставьте их.

Опубликовано на Java Code Geeks с разрешения Пирса Зайфмана, партнера нашей программы JCG. См. Оригинальную статью здесь: Применение архитектуры Android от Google с базой данных ObjectBox

Мнения, высказанные участниками Java Code Geeks, являются их собственными.