Android Core

Applying Google’s Android architecture with ObjectBox database

If you haven’t seen Google’s architecture, you can learn more about it here. Also, if you’re not familiar with ObjectBox, check out this post.

Introduction

The goal with the architecture is to end up with something like this:
 
 
 
 
 
The main difference is that I’ll be using ObjectBox instead of Room. The architecture doesn’t enforce any specific implementations though. You can always swap out the implementation details. I found ObjectBox to be one of the simplest databases, and it allows for reactive queries without depending on RxJava (though you can use RxJava if you want to).

To see the full example, you can find the GitHub repository here.

Getting started

To start, I added two models to my project: Zoo and Animal. A Zoo has a one-to-many relation with Animals. You can see their implementations below:

@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;
    }
}
@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;
    }
}

Then I created a simple MainActivity (the View layer), with a RecyclerView to display a list of Zoos, and a FloatingActionButton (FAB) to add new Zoos. I also added in some mock data for this example, so the app would have some data to display. The activity’s code is below:

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();
    }
}

I create the ViewModel for the MainActivity which is responsible for loading a list of Zoos. The activity observes this data, and when it, changes it lets the ZooAdapter know about it. I’ve left out the adapter’s and layouts’ details as they are just regular implementations. You can view the adapter here if you would like to see it.

You can see the result below:

ViewModel

The ViewModel is essentially just responsible for communicating between the repository and view. It sends requests from the view to the repository, and returns results to the view as well. I try to make one ViewModel per view, but you could use more it if makes sense to do so. All of my ViewModels extend from a BaseViewModel class which manages the ObjectBox subscriptions:

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<>();
    }
}

To really follow this type of architecture properly, I shouldn’t be exposing ObjectBox to the ViewModel layer. I left it this way to keep the example simpler, but you could abstract it out with something like RxRelay.

The ViewModel which manages my MainActivity looks like this:

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();
    }
}

It initializes itself with MutableLiveData to observe List

, and an ObjectBox DataSubscription. This subscription monitors changes in the database, it will set the MutableLiveData whenever the data changes. Which will then notify its own observers of the change. One nice thing with LiveData is that its observers are lifecycle aware, so background data updates will only be sent when the view is active.

It also exposes a method to refresh the Zoos. This would trigger the repository to refresh the data from the source. Typically a remote server, so it would send a network request to get the latest data. As a result, the ObjectBox database gets updated and relevant subscriptions will be notified. Resulting in the LiveData in my ViewModel getting updated, passing on the result to its observers, and notifying the view layer.

Repository

The repository is responsible for passing requests from the ViewModel to the database and the network, and returning the response. I broke these layers down into API classes (to access remote APIs) and DAO classes (to access the database). You can see my implementation here:

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()));
        }
    }
}

I keep most of the logic in the repository layer. The DAO and API layers only have one responsibility. For the DAO it’s interacting with the database, and for the API it’s interacting with the remote API. They are either passing data on, or returning it, they don’t manipulate it themselves. So when the repository gets a response from the API, it’s responsible for parsing that response and subsequently sending it to the DAO to update the database.

If the repository is given an Observer or LiveData, it sets its value on completion to pass the response back to the ViewModel. These response objects can contain extra information like status codes (success, failure, etc.) and error messages. Since the data models won’t contain this type of information. The implementation of these response objects really depend on what kind of information the ViewModel needs.

Parsing

To prevent the repository from becoming bloated, I split the response parsing logic into its own set of classes. These parsing classes just take a response String (likely JSON), and convert it into the appropriate model(s). For parsing Zoo responses my class looks like this:

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;
    }
}

You may notice I included a list of Animals in the parser – this is because it’s possible that a response for a Zoo could contain Animals as well. Although I didn’t actually implement any such responses in this example. The idea here is that a Parser class isn’t directly mapping JSON objects to my data models. It parses the entire response, which may include multiple data models or specific keys that don’t belong to models (like page count).

DAO

This layer is responsible for interacting with the database. I would create DAO classes for each model, like my ZooDAO:

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);
    }
}

Here you can see ObjectBox being accessed directly. It performs any required database operations, and builds query subscriptions. These queries can be observed in the ViewModel, which notifies the LiveData of any data updates.

API

The API layer is used to make requests to the network and pass those responses back to the repository. The implementation in this example is mocked out, you can see the source here. This could use any type of network requests, you could use Retrofit for example.

The repository observes the response from the API and handles them. Every API request will create a Response object:

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;
    }
}

This is the relevant information that the repository needs – the response payload and status. More statuses could be added, for example for specific error statuses like authentication failure.

Bringing it all together

In this example, I have my MainActivity observing the Zoos LiveData. It will be notified whenever this value is set. For example, if I add a new Zoo, it will automatically refresh the view. This is because I have set up a DataSubscription with ObjectBox to observe changes. You can see this in action here:

You can find the source code for this DialogFragment here. The important piece of this class is that it’s observing the response here:

...

        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;
                }
            }
        });

...

When the save action is triggered in the ViewModel, it sets the LiveData’s response with the loading status. Then when it succeeds or fails, it sets that result back as well. Once it returns back to MainActivity, you can see the new Zoo was added without having to explicitly trigger a refresh.

Wrap up

To see the full example, you can find the GitHub repository here. But, you can apply what I have explained here to any new views/models/repositories.

I believe this architecture is a bit easier to understand and apply than the fully Clean Architecture. But, the same principles apply – like isolating unique layers and giving them a single responsibility. I do not consider what I have presented here to be complete, it is meant to be a starting point. You can use it as is, but you should be thinking about how you can build and improve on it. Moving towards something like Buffer’s Clean Architecture Boilerplate is the goal. But, it can be overwhelming to grasp all of these concepts without a solid base to stand on.

Also, in an effort to make it easier to grasp, I tried to keep dependencies to a minimum. I did not include things like Retrofit, OkHttp, Butterknife, RxJava, Dagger, etc. You can add whatever libraries you like to use, you can swap out ObjectBox too. Though I think it works well with this approach.

Let me know what you think of this approach. Also if you have any comments, questions, or suggestions please post them.

Published on Java Code Geeks with permission by Pierce Zaifman, partner at our JCG program. See the original article here: Applying Google’s Android architecture with ObjectBox database

Opinions expressed by Java Code Geeks contributors are their own.

Pierce Zaifman

Pierce is a freelance Android app developer and has been developing Android apps for over 4 years. He enjoys learning new technologies and passing that knowledge on to others.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Cyrus
Cyrus
3 years ago

Thank you for sharing these Android architecture with Object Box database is whole information is very helpful for me.

Back to top button