提交 70a1072e 编写于 作者: F Florina Muntenescu

Updating the BasicSample with the latest API

上级 ba4cdb1a
......@@ -17,7 +17,7 @@ This sample contains two screens: a list of products and a detail view, that sho
#### Presentation layer
The presentation layer consists of the following components:
* A main activity that handles navigation.
* A main activity that handles navigation.
* A fragment to display the list of products.
* A fragment to display a product review.
......@@ -53,48 +53,22 @@ The app uses a Model-View-ViewModel (MVVM) architecture for the presentation lay
The database is created using Room and it has two entities: a `ProductEntity` and a `CommentEntity` that generate corresponding SQLite tables at runtime.
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..
Room populates the database asynchronously when it's created, via the `RoomDatabase#Callback`. To simulate low-performance, an artificial delay is added. To let
other components know when the data has finished populating, the `AppDatabase` exposes a
`LiveData` object..
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:
```
```java
@Query("select * from products where id = :productId")
LiveData<ProductEntity> loadProduct(int productId);
```
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.
#### Transformations
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.
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**:
```java
mObservableProducts = Transformations.switchMap(databaseCreated,
new Function<Boolean, LiveData<List<ProductEntity>>>() {
@Override
public LiveData<List<ProductEntity>> apply(Boolean isDbCreated) {
if (!isDbCreated) {
return ABSENT;
} else {
return databaseCreator.getDatabase().productDao().loadAllProducts();
}
}
});
```
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`.
Note that the first time a LiveData object is observed, the current value is emitted and `onChanged` is called.
The following diagram shows the general structure of the sample:
![ViewModel Subscriptions diagram](docs/images/VM_subscriptions.png?raw=true "ViewModel Subscriptions diagram")
Exercise for the reader: try to apply a transformation to the list of products in the ViewModel
before they are delivered to the fragment. (hint: `Transformations.Map`).
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
observes the changes of the list of products and only forwards it when the database is ready to be used.
License
--------
......
......@@ -45,6 +45,10 @@ android {
abortOnError false
}
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
}
}
dependencies {
......
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.persistence;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.Observer;
import android.support.annotation.Nullable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class LiveDataTestUtil {
/**
* Get the value from a LiveData object. We're waiting for LiveData to emit, for 2 seconds.
* Once we got a notification via onChanged, we stop observing.
*/
public static <T> T getValue(final LiveData<T> liveData) throws InterruptedException {
final Object[] data = new Object[1];
final CountDownLatch latch = new CountDownLatch(1);
Observer<T> observer = new Observer<T>() {
@Override
public void onChanged(@Nullable T o) {
data[0] = o;
latch.countDown();
liveData.removeObserver(this);
}
};
liveData.observeForever(observer);
latch.await(2, TimeUnit.SECONDS);
//noinspection unchecked
return (T) data[0];
}
}
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.persistence.db;
import static com.example.android.persistence.db.TestData.COMMENTS;
import static com.example.android.persistence.db.TestData.COMMENT_ENTITY;
import static com.example.android.persistence.db.TestData.PRODUCTS;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import android.arch.core.executor.testing.InstantTaskExecutorRule;
import android.arch.persistence.room.Room;
import android.database.sqlite.SQLiteConstraintException;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import com.example.android.persistence.LiveDataTestUtil;
import com.example.android.persistence.db.dao.CommentDao;
import com.example.android.persistence.db.dao.ProductDao;
import com.example.android.persistence.db.entity.CommentEntity;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
/**
* Test the implementation of {@link CommentDao}
*/
@RunWith(AndroidJUnit4.class)
public class CommentDaoTest {
@Rule
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
private AppDatabase mDatabase;
private CommentDao mCommentDao;
private ProductDao mProductDao;
@Before
public void initDb() throws Exception {
// using an in-memory database because the information stored here disappears when the
// process is killed
mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
AppDatabase.class)
// allowing main thread queries, just for testing
.allowMainThreadQueries()
.build();
mCommentDao = mDatabase.commentDao();
mProductDao = mDatabase.productDao();
}
@After
public void closeDb() throws Exception {
mDatabase.close();
}
@Test
public void getCommentsWhenNoCommentInserted() throws InterruptedException {
List<CommentEntity> comments = LiveDataTestUtil.getValue(mCommentDao.loadComments
(COMMENT_ENTITY.getProductId()));
assertTrue(comments.isEmpty());
}
@Test
public void cantInsertCommentWithoutProduct() throws InterruptedException {
try {
mCommentDao.insertAll(COMMENTS);
fail("SQLiteConstraintException expected");
} catch (SQLiteConstraintException ignored) {
}
}
@Test
public void getCommentsAfterInserted() throws InterruptedException {
mProductDao.insertAll(PRODUCTS);
mCommentDao.insertAll(COMMENTS);
List<CommentEntity> comments = LiveDataTestUtil.getValue(mCommentDao.loadComments
(COMMENT_ENTITY.getProductId()));
assertThat(comments.size(), is(1));
}
@Test
public void getCommentByProductId() throws InterruptedException {
mProductDao.insertAll(PRODUCTS);
mCommentDao.insertAll(COMMENTS);
List<CommentEntity> comments = LiveDataTestUtil.getValue(mCommentDao.loadComments(
(COMMENT_ENTITY.getProductId())));
assertThat(comments.size(), is(1));
}
}
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.persistence.db;
import static com.example.android.persistence.db.TestData.PRODUCTS;
import static com.example.android.persistence.db.TestData.PRODUCT_ENTITY;
import static junit.framework.Assert.assertTrue;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import android.arch.core.executor.testing.InstantTaskExecutorRule;
import android.arch.persistence.room.Room;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import com.example.android.persistence.LiveDataTestUtil;
import com.example.android.persistence.db.dao.ProductDao;
import com.example.android.persistence.db.entity.ProductEntity;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
/**
* Test the implementation of {@link ProductDao}
*/
@RunWith(AndroidJUnit4.class)
public class ProductDaoTest {
@Rule
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
private AppDatabase mDatabase;
private ProductDao mProductDao;
@Before
public void initDb() throws Exception {
// using an in-memory database because the information stored here disappears when the
// process is killed
mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
AppDatabase.class)
// allowing main thread queries, just for testing
.allowMainThreadQueries()
.build();
mProductDao = mDatabase.productDao();
}
@After
public void closeDb() throws Exception {
mDatabase.close();
}
@Test
public void getProductsWhenNoProductInserted() throws InterruptedException {
List<ProductEntity> products = LiveDataTestUtil.getValue(mProductDao.loadAllProducts());
assertTrue(products.isEmpty());
}
@Test
public void getProductsAfterInserted() throws InterruptedException {
mProductDao.insertAll(PRODUCTS);
List<ProductEntity> products = LiveDataTestUtil.getValue(mProductDao.loadAllProducts());
assertThat(products.size(), is(PRODUCTS.size()));
}
@Test
public void getProductById() throws InterruptedException {
mProductDao.insertAll(PRODUCTS);
ProductEntity product = LiveDataTestUtil.getValue(mProductDao.loadProduct
(PRODUCT_ENTITY.getId()));
assertThat(product.getId(), is(PRODUCT_ENTITY.getId()));
assertThat(product.getName(), is(PRODUCT_ENTITY.getName()));
assertThat(product.getDescription(), is(PRODUCT_ENTITY.getDescription()));
assertThat(product.getPrice(), is(PRODUCT_ENTITY.getPrice()));
}
}
package com.example.android.persistence.db;
import com.example.android.persistence.db.entity.CommentEntity;
import com.example.android.persistence.db.entity.ProductEntity;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* Utility class that holds values to be used for testing.
*/
public class TestData {
static final ProductEntity PRODUCT_ENTITY = new ProductEntity(1, "name", "desc",
3);
static final ProductEntity PRODUCT_ENTITY2 = new ProductEntity(2, "name2", "desc2",
20);
static final List<ProductEntity> PRODUCTS = Arrays.asList(PRODUCT_ENTITY, PRODUCT_ENTITY2);
static final CommentEntity COMMENT_ENTITY = new CommentEntity(1, PRODUCT_ENTITY.getId(),
"desc", new Date());
static final CommentEntity COMMENT_ENTITY2 = new CommentEntity(2,
PRODUCT_ENTITY2.getId(), "desc2", new Date());
static final List<CommentEntity> COMMENTS = Arrays.asList(COMMENT_ENTITY, COMMENT_ENTITY2);
}
......@@ -14,36 +14,20 @@
* limitations under the License.
*/
package com.example.android.persistence;
package com.example.android.persistence.ui;
import android.arch.core.executor.testing.CountingTaskExecutorRule;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.Observer;
import android.arch.lifecycle.ViewModelProviders;
import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.IdlingRegistry;
import android.support.test.espresso.IdlingResource;
import android.support.test.espresso.contrib.RecyclerViewActions;
import android.support.test.espresso.matcher.ViewMatchers;
import android.support.test.rule.ActivityTestRule;
import android.support.v4.app.Fragment;
import com.example.android.persistence.db.DatabaseCreator;
import com.example.android.persistence.db.entity.ProductEntity;
import com.example.android.persistence.viewmodel.ProductListViewModel;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
......@@ -53,6 +37,10 @@ import static android.support.test.espresso.matcher.ViewMatchers.withContentDesc
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.core.IsNot.not;
import com.example.android.persistence.EspressoTestUtil;
import com.example.android.persistence.R;
import com.example.android.persistence.ui.MainActivity;
public class MainActivityTest {
@Rule
......@@ -62,30 +50,6 @@ public class MainActivityTest {
@Rule
public CountingTaskExecutorRule mCountingTaskExecutorRule = new CountingTaskExecutorRule();
@Before
public void waitForDbCreation() throws Throwable {
final CountDownLatch latch = new CountDownLatch(1);
final LiveData<Boolean> databaseCreated = DatabaseCreator.getInstance(
InstrumentationRegistry.getTargetContext())
.isDatabaseCreated();
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
databaseCreated.observeForever(new Observer<Boolean>() {
@Override
public void onChanged(@Nullable Boolean aBoolean) {
if (Boolean.TRUE.equals(aBoolean)) {
databaseCreated.removeObserver(this);
latch.countDown();
}
}
});
}
});
MatcherAssert.assertThat("database should've initialized",
latch.await(1, TimeUnit.MINUTES), CoreMatchers.is(true));
}
@Before
public void disableRecyclerViewAnimations() {
EspressoTestUtil.disableAnimations(mActivityRule);
......@@ -95,7 +59,7 @@ public class MainActivityTest {
public void clickOnFirstItem_opensComments() throws TimeoutException, InterruptedException {
drain();
// When clicking on the first product
onView(withContentDescription(R.string.cd_products_list))
onView(ViewMatchers.withContentDescription(R.string.cd_products_list))
.perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
drain();
// Then the second screen with the comments should appear.
......
......@@ -18,21 +18,22 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.persistence">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-feature android:name="android.hardware.location.gps" />
<uses-feature android:name="android.hardware.location.gps"/>
<application
android:name=".BasicApp"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name="com.example.android.persistence.MainActivity">
<activity android:name="com.example.android.persistence.ui.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
......
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.persistence;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* Global executor pools for the whole application.
* <p>
* Grouping tasks like this avoids the effects of task starvation (e.g. disk reads don't wait behind
* webservice requests).
*/
public class AppExecutors {
private final Executor mDiskIO;
private final Executor mNetworkIO;
private final Executor mMainThread;
private AppExecutors(Executor diskIO, Executor networkIO, Executor mainThread) {
this.mDiskIO = diskIO;
this.mNetworkIO = networkIO;
this.mMainThread = mainThread;
}
public AppExecutors() {
this(Executors.newSingleThreadExecutor(), Executors.newFixedThreadPool(3),
new MainThreadExecutor());
}
public Executor diskIO() {
return mDiskIO;
}
public Executor networkIO() {
return mNetworkIO;
}
public Executor mainThread() {
return mMainThread;
}
private static class MainThreadExecutor implements Executor {
private Handler mainThreadHandler = new Handler(Looper.getMainLooper());
@Override
public void execute(@NonNull Runnable command) {
mainThreadHandler.post(command);
}
}
}
/*
* Copyright 2017, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.persistence;
import android.app.Application;
import com.example.android.persistence.db.AppDatabase;
/**
* Android Application class. Used for accessing singletons.
*/
public class BasicApp extends Application {
private AppExecutors mAppExecutors;
@Override
public void onCreate() {
super.onCreate();
mAppExecutors = new AppExecutors();
}
public AppDatabase getDatabase() {
return AppDatabase.getInstance(this, mAppExecutors);
}
public DataRepository getRepository() {
return DataRepository.getInstance(getDatabase());
}
}
package com.example.android.persistence;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MediatorLiveData;
import com.example.android.persistence.db.AppDatabase;
import com.example.android.persistence.db.entity.CommentEntity;
import com.example.android.persistence.db.entity.ProductEntity;
import java.util.List;
/**
* Repository handling the work with products and comments.
*/
public class DataRepository {
private static DataRepository sInstance;
private final AppDatabase mDatabase;
private MediatorLiveData<List<ProductEntity>> mObservableProducts;
private DataRepository(final AppDatabase database) {
mDatabase = database;
mObservableProducts = new MediatorLiveData<>();
mObservableProducts.addSource(mDatabase.productDao().loadAllProducts(),
productEntities -> {
if (mDatabase.getDatabaseCreated().getValue() != null) {
mObservableProducts.postValue(productEntities);
}
});
}
public static DataRepository getInstance(final AppDatabase database) {
if (sInstance == null) {
synchronized (DataRepository.class) {
if (sInstance == null) {
sInstance = new DataRepository(database);
}
}
}
return sInstance;
}
/**
* Get the list of products from the database and get notified when the data changes.
*/
public LiveData<List<ProductEntity>> getProducts() {
return mObservableProducts;
}
public LiveData<ProductEntity> loadProduct(final int productId) {
return mDatabase.productDao().loadProduct(productId);
}
public LiveData<List<CommentEntity>> loadComments(final int productId) {
return mDatabase.commentDao().loadComments(productId);
}
}
......@@ -16,23 +16,109 @@
package com.example.android.persistence.db;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.room.Database;
import android.arch.persistence.room.Room;
import android.arch.persistence.room.RoomDatabase;
import android.arch.persistence.room.TypeConverters;
import android.content.Context;
import android.support.annotation.NonNull;
import com.example.android.persistence.AppExecutors;
import com.example.android.persistence.db.converter.DateConverter;
import com.example.android.persistence.db.dao.CommentDao;
import com.example.android.persistence.db.dao.ProductDao;
import com.example.android.persistence.db.entity.CommentEntity;
import com.example.android.persistence.db.entity.ProductEntity;
import com.example.android.persistence.db.converter.DateConverter;
import java.util.List;
@Database(entities = {ProductEntity.class, CommentEntity.class}, version = 1)
@TypeConverters(DateConverter.class)
public abstract class AppDatabase extends RoomDatabase {
static final String DATABASE_NAME = "basic-sample-db";
private static AppDatabase sInstance;
private static final String DATABASE_NAME = "basic-sample-db";
public abstract ProductDao productDao();
public abstract CommentDao commentDao();
private final MutableLiveData<Boolean> mIsDatabaseCreated = new MutableLiveData<>();
public static AppDatabase getInstance(final Context context, final AppExecutors executors) {
if (sInstance == null) {
synchronized (AppDatabase.class) {
if (sInstance == null) {
sInstance = buildDatabase(context.getApplicationContext(), executors);
sInstance.updateDatabaseCreated(context.getApplicationContext());
}
}
}
return sInstance;
}
/**
* Build the database. {@link Builder#build()} only sets up the database configuration and
* creates a new instance of the database.
* The SQLite database is only created when it's accessed for the first time.
*/
private static AppDatabase buildDatabase(final Context appContext,
final AppExecutors executors) {
return Room.databaseBuilder(appContext, AppDatabase.class, DATABASE_NAME)
.addCallback(new Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
executors.diskIO().execute(() -> {
// Add a delay to simulate a long-running operation
addDelay();
// Generate the data for pre-population
AppDatabase database = AppDatabase.getInstance(appContext, executors);
List<ProductEntity> products = DataGenerator.generateProducts();
List<CommentEntity> comments =
DataGenerator.generateCommentsForProducts(products);
insertData(database, products, comments);
// notify that the database was created and it's ready to be used
database.setDatabaseCreated();
});
}
}).build();
}
/**
* Check whether the database already exists and expose it via {@link #getDatabaseCreated()}
*/
private void updateDatabaseCreated(final Context context) {
if (context.getDatabasePath(DATABASE_NAME).exists()) {
setDatabaseCreated();
}
}
private void setDatabaseCreated(){
mIsDatabaseCreated.postValue(true);
}
private static void insertData(final AppDatabase database, final List<ProductEntity> products,
final List<CommentEntity> comments) {
database.runInTransaction(() -> {
database.productDao().insertAll(products);
database.commentDao().insertAll(comments);
});
}
private static void addDelay() {
try {
Thread.sleep(4000);
} catch (InterruptedException ignored) {
}
}
public LiveData<Boolean> getDatabaseCreated() {
return mIsDatabaseCreated;
}
}
......@@ -26,8 +26,10 @@ import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/** Generates dummy data and inserts them into the database */
class DatabaseInitUtil {
/**
* Generates data to pre-populate the database
*/
public class DataGenerator {
private static final String[] FIRST = new String[]{
"Special edition", "New", "Cheap", "Quality", "Used"};
......@@ -40,16 +42,8 @@ class DatabaseInitUtil {
"Comment 1", "Comment 2", "Comment 3", "Comment 4", "Comment 5", "Comment 6",
};
static void initializeDb(AppDatabase db) {
public static List<ProductEntity> generateProducts() {
List<ProductEntity> products = new ArrayList<>(FIRST.length * SECOND.length);
List<CommentEntity> comments = new ArrayList<>();
generateData(products, comments);
insertData(db, products, comments);
}
private static void generateData(List<ProductEntity> products, List<CommentEntity> comments) {
Random rnd = new Random();
for (int i = 0; i < FIRST.length; i++) {
for (int j = 0; j < SECOND.length; j++) {
......@@ -61,6 +55,13 @@ class DatabaseInitUtil {
products.add(product);
}
}
return products;
}
public static List<CommentEntity> generateCommentsForProducts(
final List<ProductEntity> products) {
List<CommentEntity> comments = new ArrayList<>();
Random rnd = new Random();
for (Product product : products) {
int commentsNumber = rnd.nextInt(5) + 1;
......@@ -73,16 +74,7 @@ class DatabaseInitUtil {
comments.add(comment);
}
}
}
private static void insertData(AppDatabase db, List<ProductEntity> products, List<CommentEntity> comments) {
db.beginTransaction();
try {
db.productDao().insertAll(products);
db.commentDao().insertAll(comments);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return comments;
}
}
/*
* Copyright 2017, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.persistence.db;
import android.annotation.SuppressLint;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.persistence.room.Room;
import android.content.Context;
import android.os.AsyncTask;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.example.android.persistence.db.AppDatabase.DATABASE_NAME;
/**
* Creates the {@link AppDatabase} asynchronously, exposing a LiveData object to notify of creation.
*/
public class DatabaseCreator {
private static DatabaseCreator sInstance;
private final MutableLiveData<Boolean> mIsDatabaseCreated = new MutableLiveData<>();
private AppDatabase mDb;
private final AtomicBoolean mInitializing = new AtomicBoolean(true);
// For Singleton instantiation
private static final Object LOCK = new Object();
public synchronized static DatabaseCreator getInstance(Context context) {
if (sInstance == null) {
synchronized (LOCK) {
if (sInstance == null) {
sInstance = new DatabaseCreator();
}
}
}
return sInstance;
}
/** Used to observe when the database initialization is done */
public LiveData<Boolean> isDatabaseCreated() {
return mIsDatabaseCreated;
}
@Nullable
public AppDatabase getDatabase() {
return mDb;
}
/**
* Creates or returns a previously-created database.
* <p>
* Although this uses an AsyncTask which currently uses a serial executor, it's thread-safe.
*/
@SuppressLint("StaticFieldLeak")
public void createDb(Context context) {
Log.d("DatabaseCreator", "Creating DB from " + Thread.currentThread().getName());
if (!mInitializing.compareAndSet(true, false)) {
return; // Already initializing
}
mIsDatabaseCreated.setValue(false);// Trigger an update to show a loading screen.
new AsyncTask<Context, Void, Void>() {
@Override
protected Void doInBackground(Context... params) {
Log.d("DatabaseCreator",
"Starting bg job " + Thread.currentThread().getName());
Context context = params[0].getApplicationContext();
// Reset the database to have new data on every run.
context.deleteDatabase(DATABASE_NAME);
// Build the database!
AppDatabase db = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, DATABASE_NAME).build();
// Add a delay to simulate a long-running operation
addDelay();
// Add some data to the database
DatabaseInitUtil.initializeDb(db);
Log.d("DatabaseCreator",
"DB was populated in thread " + Thread.currentThread().getName());
mDb = db;
return null;
}
@Override
protected void onPostExecute(Void ignored) {
// Now on the main thread, notify observers that the db is created and ready.
mIsDatabaseCreated.setValue(true);
}
}.execute(context.getApplicationContext());
}
private void addDelay() {
try {
Thread.sleep(4000);
} catch (InterruptedException ignored) {}
}
}
......@@ -16,7 +16,6 @@
package com.example.android.persistence.db.dao;
import android.arch.lifecycle.LiveData;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Insert;
......
......@@ -20,17 +20,19 @@ import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import com.example.android.persistence.model.Comment;
import java.util.Date;
@Entity(tableName = "comments", foreignKeys = {
@ForeignKey(entity = ProductEntity.class,
parentColumns = "id",
childColumns = "productId",
onDelete = ForeignKey.CASCADE)}, indices = {
@Index(value = "productId")
})
@Entity(tableName = "comments",
foreignKeys = {
@ForeignKey(entity = ProductEntity.class,
parentColumns = "id",
childColumns = "productId",
onDelete = ForeignKey.CASCADE)},
indices = {@Index(value = "productId")
})
public class CommentEntity implements Comment {
@PrimaryKey(autoGenerate = true)
private int id;
......@@ -77,10 +79,10 @@ public class CommentEntity implements Comment {
public CommentEntity() {
}
public CommentEntity(Comment comment) {
id = comment.getId();
productId = comment.getProductId();
text = comment.getText();
postedAt = comment.getPostedAt();
public CommentEntity(int id, int productId, String text, Date postedAt) {
this.id = id;
this.productId = productId;
this.text = text;
this.postedAt = postedAt;
}
}
......@@ -68,6 +68,13 @@ public class ProductEntity implements Product {
public ProductEntity() {
}
public ProductEntity(int id, String name, String description, int price) {
this.id = id;
this.name = name;
this.description = description;
this.price = price;
}
public ProductEntity(Product product) {
this.id = product.getId();
this.name = product.getName();
......
......@@ -14,15 +14,19 @@
* limitations under the License.
*/
package com.example.android.persistence;
package com.example.android.persistence.ui;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.arch.lifecycle.LifecycleActivity;
import android.support.v4.app.FragmentActivity;
import android.support.v7.app.AppCompatActivity;
import com.example.android.persistence.R;
import com.example.android.persistence.model.Product;
public class MainActivity extends LifecycleActivity {
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
......
......@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.example.android.persistence;
package com.example.android.persistence.ui;
import android.arch.lifecycle.LifecycleFragment;
import android.arch.lifecycle.Observer;
......@@ -22,21 +22,21 @@ import android.arch.lifecycle.ViewModelProviders;
import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.example.android.persistence.R;
import com.example.android.persistence.databinding.ProductFragmentBinding;
import com.example.android.persistence.db.entity.CommentEntity;
import com.example.android.persistence.db.entity.ProductEntity;
import com.example.android.persistence.model.Comment;
import com.example.android.persistence.ui.CommentAdapter;
import com.example.android.persistence.ui.CommentClickCallback;
import com.example.android.persistence.viewmodel.ProductViewModel;
import java.util.List;
public class ProductFragment extends LifecycleFragment {
public class ProductFragment extends Fragment {
private static final String KEY_PRODUCT_ID = "product_id";
......@@ -68,6 +68,7 @@ public class ProductFragment extends LifecycleFragment {
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ProductViewModel.Factory factory = new ProductViewModel.Factory(
getActivity().getApplication(), getArguments().getInt(KEY_PRODUCT_ID));
......
......@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.example.android.persistence;
package com.example.android.persistence.ui;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleFragment;
......@@ -23,20 +23,20 @@ import android.arch.lifecycle.ViewModelProviders;
import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.example.android.persistence.R;
import com.example.android.persistence.databinding.ListFragmentBinding;
import com.example.android.persistence.db.entity.ProductEntity;
import com.example.android.persistence.model.Product;
import com.example.android.persistence.ui.ProductAdapter;
import com.example.android.persistence.ui.ProductClickCallback;
import com.example.android.persistence.viewmodel.ProductListViewModel;
import java.util.List;
public class ProductListFragment extends LifecycleFragment {
public class ProductListFragment extends Fragment {
public static final String TAG = "ProductListViewModel";
......
......@@ -17,48 +17,32 @@
package com.example.android.persistence.viewmodel;
import android.app.Application;
import android.arch.core.util.Function;
import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Transformations;
import android.arch.lifecycle.MediatorLiveData;
import com.example.android.persistence.db.DatabaseCreator;
import com.example.android.persistence.BasicApp;
import com.example.android.persistence.db.entity.ProductEntity;
import java.util.List;
public class ProductListViewModel extends AndroidViewModel {
private static final MutableLiveData ABSENT = new MutableLiveData();
{
//noinspection unchecked
ABSENT.setValue(null);
}
private final LiveData<List<ProductEntity>> mObservableProducts;
// MediatorLiveData can observe other LiveData objects and react on their emissions.
private final MediatorLiveData<List<ProductEntity>> mObservableProducts;
public ProductListViewModel(Application application) {
super(application);
final DatabaseCreator databaseCreator = DatabaseCreator.getInstance(this.getApplication());
mObservableProducts = new MediatorLiveData<>();
// set by default null, until we get data from the database.
mObservableProducts.setValue(null);
LiveData<Boolean> databaseCreated = databaseCreator.isDatabaseCreated();
mObservableProducts = Transformations.switchMap(databaseCreated,
new Function<Boolean, LiveData<List<ProductEntity>>>() {
@Override
public LiveData<List<ProductEntity>> apply(Boolean isDbCreated) {
if (!Boolean.TRUE.equals(isDbCreated)) { // Not needed here, but watch out for null
//noinspection unchecked
return ABSENT;
} else {
//noinspection ConstantConditions
return databaseCreator.getDatabase().productDao().loadAllProducts();
}
}
});
LiveData<List<ProductEntity>> products = ((BasicApp) application).getRepository()
.getProducts();
databaseCreator.createDb(this.getApplication());
// observe the changes of the products from the database and forward them
mObservableProducts.addSource(products, mObservableProducts::setValue);
}
/**
......
......@@ -17,17 +17,15 @@
package com.example.android.persistence.viewmodel;
import android.app.Application;
import android.arch.core.util.Function;
import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Transformations;
import android.arch.lifecycle.ViewModel;
import android.arch.lifecycle.ViewModelProvider;
import android.databinding.ObservableField;
import android.support.annotation.NonNull;
import com.example.android.persistence.db.DatabaseCreator;
import com.example.android.persistence.BasicApp;
import com.example.android.persistence.DataRepository;
import com.example.android.persistence.db.entity.CommentEntity;
import com.example.android.persistence.db.entity.ProductEntity;
......@@ -35,12 +33,6 @@ import java.util.List;
public class ProductViewModel extends AndroidViewModel {
private static final MutableLiveData ABSENT = new MutableLiveData();
{
//noinspection unchecked
ABSENT.setValue(null);
}
private final LiveData<ProductEntity> mObservableProduct;
public ObservableField<ProductEntity> product = new ObservableField<>();
......@@ -49,42 +41,15 @@ public class ProductViewModel extends AndroidViewModel {
private final LiveData<List<CommentEntity>> mObservableComments;
public ProductViewModel(@NonNull Application application,
final int productId) {
public ProductViewModel(@NonNull Application application, DataRepository repository,
final int productId) {
super(application);
mProductId = productId;
final DatabaseCreator databaseCreator = DatabaseCreator.getInstance(this.getApplication());
mObservableComments = Transformations.switchMap(databaseCreator.isDatabaseCreated(), new Function<Boolean, LiveData<List<CommentEntity>>>() {
@Override
public LiveData<List<CommentEntity>> apply(Boolean isDbCreated) {
if (!isDbCreated) {
//noinspection unchecked
return ABSENT;
} else {
//noinspection ConstantConditions
return databaseCreator.getDatabase().commentDao().loadComments(mProductId);
}
}
});
mObservableProduct = Transformations.switchMap(databaseCreator.isDatabaseCreated(), new Function<Boolean, LiveData<ProductEntity>>() {
@Override
public LiveData<ProductEntity> apply(Boolean isDbCreated) {
if (!isDbCreated) {
//noinspection unchecked
return ABSENT;
} else {
//noinspection ConstantConditions
return databaseCreator.getDatabase().productDao().loadProduct(mProductId);
}
}
});
databaseCreator.createDb(this.getApplication());
mObservableComments = repository.loadComments(mProductId);
mObservableProduct = repository.loadProduct(mProductId);
}
/**
* Expose the LiveData Comments query so the UI can observe it.
*/
......@@ -113,15 +78,18 @@ public class ProductViewModel extends AndroidViewModel {
private final int mProductId;
private final DataRepository mRepository;
public Factory(@NonNull Application application, int productId) {
mApplication = application;
mProductId = productId;
mRepository = ((BasicApp) application).getRepository();
}
@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
//noinspection unchecked
return (T) new ProductViewModel(mApplication, mProductId);
return (T) new ProductViewModel(mApplication, mRepository, mProductId);
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册