Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1532c56

Browse files
authoredNov 14, 2017
Merge pull request #231 from googlesamples/basic_update
Updating the BasicSample with the latest API
2 parents ba4cdb1 + 055b8c5 commit 1532c56

22 files changed

+686
-310
lines changed
 

‎BasicSample/README.md

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ This sample contains two screens: a list of products and a detail view, that sho
1717
#### Presentation layer
1818

1919
The presentation layer consists of the following components:
20-
* A main activity that handles navigation.
20+
* A main activity that handles navigation.
2121
* A fragment to display the list of products.
2222
* A fragment to display a product review.
2323

@@ -53,48 +53,22 @@ The app uses a Model-View-ViewModel (MVVM) architecture for the presentation lay
5353

5454
The database is created using Room and it has two entities: a `ProductEntity` and a `CommentEntity` that generate corresponding SQLite tables at runtime.
5555

56-
Room populates the database asynchronously on first use. The `DatabaseCreator` class is responsible for creating the database and tables, and populating them with sample product and review data. This is done on the first use of the database, with the help of an `AsyncTask`. To simulate low-performance, an artificial delay is added. To let other components know when the data has finished populating, the `DatabaseCreator` exposes a `LiveData` object..
56+
Room populates the database asynchronously when it's created, via the `RoomDatabase#Callback`. To simulate low-performance, an artificial delay is added. To let
57+
other components know when the data has finished populating, the `AppDatabase` exposes a
58+
`LiveData` object..
5759

5860
To access the data and execute queries, you use a [Data Access Object](https://developer.android.com/topic/libraries/architecture/room.html#daos) (DAO). For example, a product is loaded with the following query:
5961

60-
```
62+
```java
6163
@Query("select * from products where id = :productId")
6264
LiveData<ProductEntity> loadProduct(int productId);
6365
```
6466

6567
Queries that return a `LiveData` object can be observed, so when a change in one of the affected tables is detected, `LiveData` delivers a notification of that change to the registered observers.
6668

67-
#### Transformations
68-
69-
Fragments don't observe the database directly, they only interact with ViewModel objects. A ViewModel observes database queries as well as the `DatabaseCreator`, which exposes whether the database is created or not.
70-
71-
For the purpose of the sample, the database is deleted and re-populated each time the app is started, so the app needs to wait until this process is finished. This is solved with a **Transformation**:
72-
73-
```java
74-
mObservableProducts = Transformations.switchMap(databaseCreated,
75-
new Function<Boolean, LiveData<List<ProductEntity>>>() {
76-
@Override
77-
public LiveData<List<ProductEntity>> apply(Boolean isDbCreated) {
78-
if (!isDbCreated) {
79-
return ABSENT;
80-
} else {
81-
return databaseCreator.getDatabase().productDao().loadAllProducts();
82-
}
83-
}
84-
});
85-
```
86-
87-
Whenever `databaseCreated` changes, `mObservableProducts` will get a new value, either an `ABSENT` `LiveData` or the list of products. The database will be observed with the same scope as `mObservableProducts`.
88-
89-
Note that the first time a LiveData object is observed, the current value is emitted and `onChanged` is called.
90-
91-
The following diagram shows the general structure of the sample:
92-
93-
94-
![ViewModel Subscriptions diagram](docs/images/VM_subscriptions.png?raw=true "ViewModel Subscriptions diagram")
95-
96-
Exercise for the reader: try to apply a transformation to the list of products in the ViewModel
97-
before they are delivered to the fragment. (hint: `Transformations.Map`).
69+
The `DataRepository` exposes the data to the UI layer. To ensure that the UI uses the list of products only after the database has been pre-populated, a [`MediatorLiveData`](https://developer.android.com/reference/android/arch/lifecycle/MediatorLiveData.html) object is used. This
70+
observes the changes of the list of products and only forwards it when the database is ready to be used.
71+
9872

9973
License
10074
--------

‎BasicSample/app/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ android {
4545
abortOnError false
4646
}
4747

48+
compileOptions {
49+
targetCompatibility 1.8
50+
sourceCompatibility 1.8
51+
}
4852
}
4953

5054
dependencies {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (C) 2017 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.android.persistence;
18+
19+
import android.arch.lifecycle.LiveData;
20+
import android.arch.lifecycle.Observer;
21+
import android.support.annotation.Nullable;
22+
23+
import java.util.concurrent.CountDownLatch;
24+
import java.util.concurrent.TimeUnit;
25+
26+
public class LiveDataTestUtil {
27+
28+
/**
29+
* Get the value from a LiveData object. We're waiting for LiveData to emit, for 2 seconds.
30+
* Once we got a notification via onChanged, we stop observing.
31+
*/
32+
public static <T> T getValue(final LiveData<T> liveData) throws InterruptedException {
33+
final Object[] data = new Object[1];
34+
final CountDownLatch latch = new CountDownLatch(1);
35+
Observer<T> observer = new Observer<T>() {
36+
@Override
37+
public void onChanged(@Nullable T o) {
38+
data[0] = o;
39+
latch.countDown();
40+
liveData.removeObserver(this);
41+
}
42+
};
43+
liveData.observeForever(observer);
44+
latch.await(2, TimeUnit.SECONDS);
45+
//noinspection unchecked
46+
return (T) data[0];
47+
}
48+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright (C) 2017 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.android.persistence.db;
18+
19+
import static com.example.android.persistence.db.TestData.COMMENTS;
20+
import static com.example.android.persistence.db.TestData.COMMENT_ENTITY;
21+
import static com.example.android.persistence.db.TestData.PRODUCTS;
22+
23+
import static junit.framework.Assert.assertTrue;
24+
import static junit.framework.Assert.fail;
25+
26+
import static org.hamcrest.Matchers.is;
27+
import static org.junit.Assert.assertThat;
28+
29+
import android.arch.core.executor.testing.InstantTaskExecutorRule;
30+
import android.arch.persistence.room.Room;
31+
import android.database.sqlite.SQLiteConstraintException;
32+
import android.support.test.InstrumentationRegistry;
33+
import android.support.test.runner.AndroidJUnit4;
34+
35+
import com.example.android.persistence.LiveDataTestUtil;
36+
import com.example.android.persistence.db.dao.CommentDao;
37+
import com.example.android.persistence.db.dao.ProductDao;
38+
import com.example.android.persistence.db.entity.CommentEntity;
39+
40+
import org.junit.After;
41+
import org.junit.Before;
42+
import org.junit.Rule;
43+
import org.junit.Test;
44+
import org.junit.runner.RunWith;
45+
46+
import java.util.List;
47+
48+
/**
49+
* Test the implementation of {@link CommentDao}
50+
*/
51+
@RunWith(AndroidJUnit4.class)
52+
public class CommentDaoTest {
53+
54+
@Rule
55+
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
56+
57+
private AppDatabase mDatabase;
58+
59+
private CommentDao mCommentDao;
60+
61+
private ProductDao mProductDao;
62+
63+
@Before
64+
public void initDb() throws Exception {
65+
// using an in-memory database because the information stored here disappears when the
66+
// process is killed
67+
mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
68+
AppDatabase.class)
69+
// allowing main thread queries, just for testing
70+
.allowMainThreadQueries()
71+
.build();
72+
73+
mCommentDao = mDatabase.commentDao();
74+
mProductDao = mDatabase.productDao();
75+
}
76+
77+
@After
78+
public void closeDb() throws Exception {
79+
mDatabase.close();
80+
}
81+
82+
@Test
83+
public void getCommentsWhenNoCommentInserted() throws InterruptedException {
84+
List<CommentEntity> comments = LiveDataTestUtil.getValue(mCommentDao.loadComments
85+
(COMMENT_ENTITY.getProductId()));
86+
87+
assertTrue(comments.isEmpty());
88+
}
89+
90+
@Test
91+
public void cantInsertCommentWithoutProduct() throws InterruptedException {
92+
try {
93+
mCommentDao.insertAll(COMMENTS);
94+
fail("SQLiteConstraintException expected");
95+
} catch (SQLiteConstraintException ignored) {
96+
97+
}
98+
}
99+
100+
@Test
101+
public void getCommentsAfterInserted() throws InterruptedException {
102+
mProductDao.insertAll(PRODUCTS);
103+
mCommentDao.insertAll(COMMENTS);
104+
105+
List<CommentEntity> comments = LiveDataTestUtil.getValue(mCommentDao.loadComments
106+
(COMMENT_ENTITY.getProductId()));
107+
108+
assertThat(comments.size(), is(1));
109+
}
110+
111+
@Test
112+
public void getCommentByProductId() throws InterruptedException {
113+
mProductDao.insertAll(PRODUCTS);
114+
mCommentDao.insertAll(COMMENTS);
115+
116+
List<CommentEntity> comments = LiveDataTestUtil.getValue(mCommentDao.loadComments(
117+
(COMMENT_ENTITY.getProductId())));
118+
119+
assertThat(comments.size(), is(1));
120+
}
121+
122+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright (C) 2017 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.android.persistence.db;
18+
19+
import static com.example.android.persistence.db.TestData.PRODUCTS;
20+
import static com.example.android.persistence.db.TestData.PRODUCT_ENTITY;
21+
22+
import static junit.framework.Assert.assertTrue;
23+
24+
import static org.hamcrest.Matchers.is;
25+
import static org.junit.Assert.assertThat;
26+
27+
import android.arch.core.executor.testing.InstantTaskExecutorRule;
28+
import android.arch.persistence.room.Room;
29+
import android.support.test.InstrumentationRegistry;
30+
import android.support.test.runner.AndroidJUnit4;
31+
32+
import com.example.android.persistence.LiveDataTestUtil;
33+
import com.example.android.persistence.db.dao.ProductDao;
34+
import com.example.android.persistence.db.entity.ProductEntity;
35+
36+
import org.junit.After;
37+
import org.junit.Before;
38+
import org.junit.Rule;
39+
import org.junit.Test;
40+
import org.junit.runner.RunWith;
41+
42+
import java.util.List;
43+
44+
/**
45+
* Test the implementation of {@link ProductDao}
46+
*/
47+
@RunWith(AndroidJUnit4.class)
48+
public class ProductDaoTest {
49+
50+
@Rule
51+
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
52+
53+
private AppDatabase mDatabase;
54+
55+
private ProductDao mProductDao;
56+
57+
@Before
58+
public void initDb() throws Exception {
59+
// using an in-memory database because the information stored here disappears when the
60+
// process is killed
61+
mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
62+
AppDatabase.class)
63+
// allowing main thread queries, just for testing
64+
.allowMainThreadQueries()
65+
.build();
66+
67+
mProductDao = mDatabase.productDao();
68+
}
69+
70+
@After
71+
public void closeDb() throws Exception {
72+
mDatabase.close();
73+
}
74+
75+
@Test
76+
public void getProductsWhenNoProductInserted() throws InterruptedException {
77+
List<ProductEntity> products = LiveDataTestUtil.getValue(mProductDao.loadAllProducts());
78+
79+
assertTrue(products.isEmpty());
80+
}
81+
82+
@Test
83+
public void getProductsAfterInserted() throws InterruptedException {
84+
mProductDao.insertAll(PRODUCTS);
85+
86+
List<ProductEntity> products = LiveDataTestUtil.getValue(mProductDao.loadAllProducts());
87+
88+
assertThat(products.size(), is(PRODUCTS.size()));
89+
}
90+
91+
@Test
92+
public void getProductById() throws InterruptedException {
93+
mProductDao.insertAll(PRODUCTS);
94+
95+
ProductEntity product = LiveDataTestUtil.getValue(mProductDao.loadProduct
96+
(PRODUCT_ENTITY.getId()));
97+
98+
assertThat(product.getId(), is(PRODUCT_ENTITY.getId()));
99+
assertThat(product.getName(), is(PRODUCT_ENTITY.getName()));
100+
assertThat(product.getDescription(), is(PRODUCT_ENTITY.getDescription()));
101+
assertThat(product.getPrice(), is(PRODUCT_ENTITY.getPrice()));
102+
}
103+
104+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.example.android.persistence.db;
2+
3+
import com.example.android.persistence.db.entity.CommentEntity;
4+
import com.example.android.persistence.db.entity.ProductEntity;
5+
6+
import java.util.Arrays;
7+
import java.util.Date;
8+
import java.util.List;
9+
10+
/**
11+
* Utility class that holds values to be used for testing.
12+
*/
13+
public class TestData {
14+
15+
static final ProductEntity PRODUCT_ENTITY = new ProductEntity(1, "name", "desc",
16+
3);
17+
static final ProductEntity PRODUCT_ENTITY2 = new ProductEntity(2, "name2", "desc2",
18+
20);
19+
20+
static final List<ProductEntity> PRODUCTS = Arrays.asList(PRODUCT_ENTITY, PRODUCT_ENTITY2);
21+
22+
static final CommentEntity COMMENT_ENTITY = new CommentEntity(1, PRODUCT_ENTITY.getId(),
23+
"desc", new Date());
24+
static final CommentEntity COMMENT_ENTITY2 = new CommentEntity(2,
25+
PRODUCT_ENTITY2.getId(), "desc2", new Date());
26+
27+
static final List<CommentEntity> COMMENTS = Arrays.asList(COMMENT_ENTITY, COMMENT_ENTITY2);
28+
29+
30+
}

0 commit comments

Comments
 (0)
Please sign in to comment.