diff --git a/.github/issue_template.md b/.github/issue_template.md
new file mode 100644
index 000000000..d1095bd3c
--- /dev/null
+++ b/.github/issue_template.md
@@ -0,0 +1,25 @@
+/* Remove this comment
+
+Reporting an issue with the Architecture Components or the samples?
+
+## Question about Architecture Components?
+
+If you want to ask how to do something, or to understand why something isn't working the way you expect it to use Stack Overflow. Post your questions using these tags:
+* Questions about the architecture guide or using components together: android-arch https://stackoverflow.com/questions/tagged/android-arch
+* Questions about the Room library: android-room https://stackoverflow.com/questions/tagged/android-room
+* Questions about the Lifecycle library: android-arch-lifecycle https://stackoverflow.com/questions/tagged/android-arch-lifecycle
+
+
+## Bug report or feature request for Architecture Components?
+
+If you find an issue with the Architecture Components libraries or consider that a feature is missing, report it using the Google issue tracker: https://issuetracker.google.com/issues/new?component=197448&template=878802
+
+Before filing a new issue, please check if it is listed in the Release Notes or reported in the issues list.
+* Release Notes: https://developer.android.com/topic/libraries/architecture/release-notes.html
+* Issues list: https://issuetracker.google.com/issues?q=componentid=197448%20status:open&s=modified_time:desc
+
+## Issue with the samples?
+
+Please add the sample name ("all", "GithubBrowserSample", "BasicRxJavaSampleKotlin", etc.) and include it in the title if it applies.
+
+*/
diff --git a/BasicRxJavaSample/.gitignore b/BasicRxJavaSample/.gitignore
new file mode 100644
index 000000000..39fb081a4
--- /dev/null
+++ b/BasicRxJavaSample/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/BasicRxJavaSample/.google/packaging.yaml b/BasicRxJavaSample/.google/packaging.yaml
new file mode 100644
index 000000000..108903dd1
--- /dev/null
+++ b/BasicRxJavaSample/.google/packaging.yaml
@@ -0,0 +1,41 @@
+#
+# 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.
+
+# GOOGLE SAMPLE PACKAGING DATA
+#
+# This file is used by Google as part of our samples packaging process.
+# End users may safely ignore this file. It has no relevance to other systems.
+---
+# Values: {DRAFT | PUBLISHED | INTERNAL | DEPRECATED | SUPERCEDED}
+status: PUBLISHED
+
+# Optional, put additional explanation here for DEPRECATED or SUPERCEDED.
+# statusNote:
+
+# See http://go/sample-categories
+technologies: [Android]
+categories: [Architecture]
+languages: [Java]
+solutions: [Mobile]
+
+# May be omitted if unpublished
+github: googlesamples/android-architecture-components
+
+# Values: BEGINNER | INTERMEDIATE | ADVANCED | EXPERT
+level: ADVANCED
+
+# Default: apache2. May be omitted for most samples.
+# Alternatives: apache2-android (for AOSP)
+license: apache2
diff --git a/BasicRxJavaSample/README.md b/BasicRxJavaSample/README.md
new file mode 100644
index 000000000..20841ce63
--- /dev/null
+++ b/BasicRxJavaSample/README.md
@@ -0,0 +1,53 @@
+Room & RxJava Sample
+=====================
+
+This is an API sample to showcase how to implement observable queries in
+[Room](https://developer.android.com/topic/libraries/architecture/room.html), with RxJava's
+[Flowable](http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Flowable.html) objects.
+
+Introduction
+-------------
+
+### Functionality
+The sample app shows an editable user name, stored in the database.
+
+### Implementation
+
+#### Data layer
+
+The database is created using Room and has one entity: a `User`. Room generates the corresponding SQLite table at
+runtime.
+
+Queries are executed in the `UserDao` class. The user retrieval is done via an observable query implemented using a
+`Flowable`. Every time the user data is updated, the Flowable object will emit automatically, allowing to update the UI
+based on the latest data. The Flowable will emit only when the query result contains at least a row. When there is no
+data to match the query, the Flowable will not emit.
+
+#### Presentation layer
+
+The app has a main Activity that displays the data.
+The Activity works with a ViewModel to do the following:
+* subscribe to the emissions of the user name and update the UI every time there is a new user name emitted
+* notify the ViewModel when the "Update" button is pressed and pass the new user name.
+The ViewModel works with the data source to get and save the data.
+
+Room guarantees that the observable query will be triggered on a background thread. In the Activity, the Flowable events
+are set to be received on the main thread, so the UI can be updated. The insert query is synchronous so it's wrapped in
+a Completable and executed on a background thread. On completion, the Activity is notified on the main thread.
+
+License
+--------
+
+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.
diff --git a/BasicRxJavaSample/app/.gitignore b/BasicRxJavaSample/app/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/BasicRxJavaSample/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/BasicRxJavaSample/app/build.gradle b/BasicRxJavaSample/app/build.gradle
new file mode 100644
index 000000000..9de3fd881
--- /dev/null
+++ b/BasicRxJavaSample/app/build.gradle
@@ -0,0 +1,77 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion build_versions.target_sdk
+ buildToolsVersion build_versions.build_tools
+
+ defaultConfig {
+ applicationId "com.example.android.persistence"
+ minSdkVersion build_versions.min_sdk
+ targetSdkVersion build_versions.target_sdk
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ // Support libraries
+ compile deps.support.app_compat
+ compile deps.support.v4
+ compile deps.support.design
+ // Architecture components
+ compile deps.lifecycle.runtime
+ compile deps.lifecycle.extensions
+ annotationProcessor deps.lifecycle.compiler
+ compile deps.room.runtime
+ annotationProcessor deps.room.compiler
+ compile deps.room.rxjava2
+
+ // RxJava
+ compile deps.rx_android
+ compile deps.rxjava2
+
+ // Dependencies for local unit tests
+ testCompile deps.junit
+ testCompile deps.hamcrest
+ testCompile deps.arch_core.testing
+ testCompile deps.mockito.all
+
+ // Resolve conflicts between main and local unit tests
+ testCompile deps.support.annotations
+ testCompile deps.support.core_utils
+
+ // Android Testing Support Library's runner and rules
+ androidTestCompile deps.atsl.runner
+ androidTestCompile deps.atsl.rules
+ androidTestCompile deps.room.testing
+ androidTestCompile deps.arch_core.testing
+
+ // Dependencies for Android unit tests
+ androidTestCompile deps.junit
+ androidTestCompile deps.mockito.core, { exclude group: 'net.bytebuddy' }
+ androidTestCompile deps.dexmaker
+
+ // Espresso UI Testing
+ androidTestCompile deps.espresso.core
+ androidTestCompile deps.espresso.contrib
+ androidTestCompile deps.espresso.intents
+
+ // Resolve conflicts between main and test APK:
+ androidTestCompile deps.support.annotations
+ androidTestCompile deps.support.v4
+ androidTestCompile deps.support.app_compat
+ androidTestCompile deps.support.design
+}
diff --git a/BasicRxJavaSample/app/proguard-rules.pro b/BasicRxJavaSample/app/proguard-rules.pro
new file mode 100644
index 000000000..5e78d02af
--- /dev/null
+++ b/BasicRxJavaSample/app/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/florinam/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/BasicRxJavaSample/app/src/androidTest/java/com/example/android/observability/persistence/LocalUserDataSourceTest.java b/BasicRxJavaSample/app/src/androidTest/java/com/example/android/observability/persistence/LocalUserDataSourceTest.java
new file mode 100644
index 000000000..a5febcd21
--- /dev/null
+++ b/BasicRxJavaSample/app/src/androidTest/java/com/example/android/observability/persistence/LocalUserDataSourceTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.observability.persistence;
+
+import android.arch.core.executor.testing.InstantTaskExecutorRule;
+import android.arch.persistence.room.Room;
+import android.support.test.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Integration tests for the {@link LocalUserDataSource} implementation with Room.
+ */
+public class LocalUserDataSourceTest {
+
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ private static final User USER = new User("id", "username");
+
+ private UsersDatabase mDatabase;
+ private LocalUserDataSource mDataSource;
+
+ @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(),
+ UsersDatabase.class)
+ // allowing main thread queries, just for testing
+ .allowMainThreadQueries()
+ .build();
+ mDataSource = new LocalUserDataSource(mDatabase.userDao());
+ }
+
+ @After
+ public void closeDb() throws Exception {
+ mDatabase.close();
+ }
+
+ @Test
+ public void insertAndGetUser() {
+ // When inserting a new user in the data source
+ mDataSource.insertOrUpdateUser(USER);
+
+ // When subscribing to the emissions of the user
+ mDataSource.getUser()
+ .test()
+ // assertValue asserts that there was only one emission of the user
+ .assertValue(user -> {
+ // The emitted user is the expected one
+ return user != null && user.getId().equals(USER.getId()) &&
+ user.getUserName().equals(USER.getUserName());
+ });
+ }
+
+ @Test
+ public void updateAndGetUser() {
+ // Given that we have a user in the data source
+ mDataSource.insertOrUpdateUser(USER);
+
+ // When we are updating the name of the user
+ User updatedUser = new User(USER.getId(), "new username");
+ mDataSource.insertOrUpdateUser(updatedUser);
+
+ // When subscribing to the emissions of the user
+ mDatabase.userDao().getUser()
+ .test()
+ // assertValue asserts that there was only one emission of the user
+ .assertValue(user -> {
+ // The emitted user is the expected one
+ return user != null && user.getId().equals(USER.getId()) &&
+ user.getUserName().equals("new username");
+ });
+ }
+
+ @Test
+ public void deleteAndGetUser() {
+ // Given that we have a user in the data source
+ mDataSource.insertOrUpdateUser(USER);
+
+ //When we are deleting all users
+ mDataSource.deleteAllUsers();
+ // When subscribing to the emissions of the user
+ mDatabase.userDao().getUser()
+ .test()
+ // check that there's no user emitted
+ .assertNoValues();
+ }
+}
diff --git a/BasicRxJavaSample/app/src/androidTest/java/com/example/android/observability/persistence/UserDaoTest.java b/BasicRxJavaSample/app/src/androidTest/java/com/example/android/observability/persistence/UserDaoTest.java
new file mode 100644
index 000000000..10dc52bf5
--- /dev/null
+++ b/BasicRxJavaSample/app/src/androidTest/java/com/example/android/observability/persistence/UserDaoTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.observability.persistence;
+
+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 org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test the implementation of {@link UserDao}
+ */
+@RunWith(AndroidJUnit4.class)
+public class UserDaoTest {
+
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ private static final User USER = new User("id", "username");
+
+ private UsersDatabase mDatabase;
+
+ @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(),
+ UsersDatabase.class)
+ // allowing main thread queries, just for testing
+ .allowMainThreadQueries()
+ .build();
+ }
+
+ @After
+ public void closeDb() throws Exception {
+ mDatabase.close();
+ }
+
+ @Test
+ public void getUsersWhenNoUserInserted() {
+ mDatabase.userDao().getUser()
+ .test()
+ .assertNoValues();
+ }
+
+ @Test
+ public void insertAndGetUser() {
+ // When inserting a new user in the data source
+ mDatabase.userDao().insertUser(USER);
+
+ // When subscribing to the emissions of the user
+ mDatabase.userDao().getUser()
+ .test()
+ // assertValue asserts that there was only one emission of the user
+ .assertValue(user -> {
+ // The emitted user is the expected one
+ return user != null && user.getId().equals(USER.getId()) &&
+ user.getUserName().equals(USER.getUserName());
+ });
+ }
+
+ @Test
+ public void updateAndGetUser() {
+ // Given that we have a user in the data source
+ mDatabase.userDao().insertUser(USER);
+
+ // When we are updating the name of the user
+ User updatedUser = new User(USER.getId(), "new username");
+ mDatabase.userDao().insertUser(updatedUser);
+
+ // When subscribing to the emissions of the user
+ mDatabase.userDao().getUser()
+ .test()
+ // assertValue asserts that there was only one emission of the user
+ .assertValue(user -> {
+ // The emitted user is the expected one
+ return user != null && user.getId().equals(USER.getId()) &&
+ user.getUserName().equals("new username");
+ });
+ }
+
+ @Test
+ public void deleteAndGetUser() {
+ // Given that we have a user in the data source
+ mDatabase.userDao().insertUser(USER);
+
+ //When we are deleting all users
+ mDatabase.userDao().deleteAllUsers();
+ // When subscribing to the emissions of the user
+ mDatabase.userDao().getUser()
+ .test()
+ // check that there's no user emitted
+ .assertNoValues();
+ }
+}
diff --git a/BasicRxJavaSample/app/src/main/AndroidManifest.xml b/BasicRxJavaSample/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..06bff3124
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/AndroidManifest.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BasicRxJavaSample/app/src/main/java/com/example/android/observability/Injection.java b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/Injection.java
new file mode 100644
index 000000000..d3f3a2e4e
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/Injection.java
@@ -0,0 +1,39 @@
+/*
+ * 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.observability;
+
+import android.content.Context;
+
+import com.example.android.observability.persistence.LocalUserDataSource;
+import com.example.android.observability.persistence.UsersDatabase;
+import com.example.android.observability.ui.ViewModelFactory;
+
+/**
+ * Enables injection of data sources.
+ */
+public class Injection {
+
+ public static UserDataSource provideUserDataSource(Context context) {
+ UsersDatabase database = UsersDatabase.getInstance(context);
+ return new LocalUserDataSource(database.userDao());
+ }
+
+ public static ViewModelFactory provideViewModelFactory(Context context) {
+ UserDataSource dataSource = provideUserDataSource(context);
+ return new ViewModelFactory(dataSource);
+ }
+}
diff --git a/BasicRxJavaSample/app/src/main/java/com/example/android/observability/UserDataSource.java b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/UserDataSource.java
new file mode 100644
index 000000000..cd2f6010b
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/UserDataSource.java
@@ -0,0 +1,46 @@
+/*
+ * 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.observability;
+
+import com.example.android.observability.persistence.User;
+
+import io.reactivex.Flowable;
+
+/**
+ * Access point for managing user data.
+ */
+public interface UserDataSource {
+
+ /**
+ * Gets the user from the data source.
+ *
+ * @return the user from the data source.
+ */
+ Flowable getUser();
+
+ /**
+ * Inserts the user into the data source, or, if this is an existing user, updates it.
+ *
+ * @param user the user to be inserted or updated.
+ */
+ void insertOrUpdateUser(User user);
+
+ /**
+ * Deletes all users from the data source.
+ */
+ void deleteAllUsers();
+}
\ No newline at end of file
diff --git a/BasicRxJavaSample/app/src/main/java/com/example/android/observability/persistence/LocalUserDataSource.java b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/persistence/LocalUserDataSource.java
new file mode 100644
index 000000000..621e343b8
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/persistence/LocalUserDataSource.java
@@ -0,0 +1,48 @@
+/*
+ * 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.observability.persistence;
+
+import com.example.android.observability.UserDataSource;
+
+import io.reactivex.Flowable;
+
+/**
+ * Using the Room database as a data source.
+ */
+public class LocalUserDataSource implements UserDataSource {
+
+ private final UserDao mUserDao;
+
+ public LocalUserDataSource(UserDao userDao) {
+ mUserDao = userDao;
+ }
+
+ @Override
+ public Flowable getUser() {
+ return mUserDao.getUser();
+ }
+
+ @Override
+ public void insertOrUpdateUser(User user) {
+ mUserDao.insertUser(user);
+ }
+
+ @Override
+ public void deleteAllUsers() {
+ mUserDao.deleteAllUsers();
+ }
+}
diff --git a/BasicRxJavaSample/app/src/main/java/com/example/android/observability/persistence/User.java b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/persistence/User.java
new file mode 100644
index 000000000..90d3a102e
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/persistence/User.java
@@ -0,0 +1,59 @@
+/*
+ * 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.observability.persistence;
+
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Entity;
+import android.arch.persistence.room.Ignore;
+import android.arch.persistence.room.PrimaryKey;
+import android.support.annotation.NonNull;
+
+import java.util.UUID;
+
+/**
+ * Immutable model class for a User
+ */
+@Entity(tableName = "users")
+public class User {
+
+ @NonNull
+ @PrimaryKey
+ @ColumnInfo(name = "userid")
+ private String mId;
+
+ @ColumnInfo(name = "username")
+ private String mUserName;
+
+ @Ignore
+ public User(String userName) {
+ mId = UUID.randomUUID().toString();
+ mUserName = userName;
+ }
+
+ public User(String id, String userName) {
+ this.mId = id;
+ this.mUserName = userName;
+ }
+
+ public String getId() {
+ return mId;
+ }
+
+ public String getUserName() {
+ return mUserName;
+ }
+}
diff --git a/BasicRxJavaSample/app/src/main/java/com/example/android/observability/persistence/UserDao.java b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/persistence/UserDao.java
new file mode 100644
index 000000000..eb4af00e2
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/persistence/UserDao.java
@@ -0,0 +1,54 @@
+/*
+ * 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.observability.persistence;
+
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Insert;
+import android.arch.persistence.room.OnConflictStrategy;
+import android.arch.persistence.room.Query;
+
+import io.reactivex.Flowable;
+
+/**
+ * Data Access Object for the users table.
+ */
+@Dao
+public interface UserDao {
+
+ /**
+ * Get the user from the table. Since for simplicity we only have one user in the database,
+ * this query gets all users from the table, but limits the result to just the 1st user.
+ *
+ * @return the user from the table
+ */
+ @Query("SELECT * FROM Users LIMIT 1")
+ Flowable getUser();
+
+ /**
+ * Insert a user in the database. If the user already exists, replace it.
+ *
+ * @param user the user to be inserted.
+ */
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insertUser(User user);
+
+ /**
+ * Delete all users.
+ */
+ @Query("DELETE FROM Users")
+ void deleteAllUsers();
+}
diff --git a/BasicRxJavaSample/app/src/main/java/com/example/android/observability/persistence/UsersDatabase.java b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/persistence/UsersDatabase.java
new file mode 100644
index 000000000..028f39849
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/persistence/UsersDatabase.java
@@ -0,0 +1,47 @@
+/*
+ * 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.observability.persistence;
+
+import android.arch.persistence.room.Database;
+import android.arch.persistence.room.Room;
+import android.arch.persistence.room.RoomDatabase;
+import android.content.Context;
+
+/**
+ * The Room database that contains the Users table
+ */
+@Database(entities = {User.class}, version = 1)
+public abstract class UsersDatabase extends RoomDatabase {
+
+ private static volatile UsersDatabase INSTANCE;
+
+ public abstract UserDao userDao();
+
+ public static UsersDatabase getInstance(Context context) {
+ if (INSTANCE == null) {
+ synchronized (UsersDatabase.class) {
+ if (INSTANCE == null) {
+ INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
+ UsersDatabase.class, "Sample.db")
+ .build();
+ }
+ }
+ }
+ return INSTANCE;
+ }
+
+}
diff --git a/BasicRxJavaSample/app/src/main/java/com/example/android/observability/ui/UserActivity.java b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/ui/UserActivity.java
new file mode 100644
index 000000000..fe60b3d79
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/ui/UserActivity.java
@@ -0,0 +1,101 @@
+/*
+ * 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.observability.ui;
+
+import android.arch.lifecycle.ViewModelProviders;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.example.android.observability.Injection;
+import com.example.android.persistence.R;
+
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.schedulers.Schedulers;
+
+
+/**
+ * Main screen of the app. Displays a user name and gives the option to update the user name.
+ */
+public class UserActivity extends AppCompatActivity {
+
+ private static final String TAG = UserActivity.class.getSimpleName();
+
+ private TextView mUserName;
+
+ private EditText mUserNameInput;
+
+ private Button mUpdateButton;
+
+ private ViewModelFactory mViewModelFactory;
+
+ private UserViewModel mViewModel;
+
+ private final CompositeDisposable mDisposable = new CompositeDisposable();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_user);
+
+ mUserName = findViewById(R.id.user_name);
+ mUserNameInput = findViewById(R.id.user_name_input);
+ mUpdateButton = findViewById(R.id.update_user);
+
+ mViewModelFactory = Injection.provideViewModelFactory(this);
+ mViewModel = ViewModelProviders.of(this, mViewModelFactory).get(UserViewModel.class);
+ mUpdateButton.setOnClickListener(v -> updateUserName());
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ // Subscribe to the emissions of the user name from the view model.
+ // Update the user name text view, at every onNext emission.
+ // In case of error, log the exception.
+ mDisposable.add(mViewModel.getUserName()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(userName -> mUserName.setText(userName),
+ throwable -> Log.e(TAG, "Unable to update username", throwable)));
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ // clear all the subscriptions
+ mDisposable.clear();
+ }
+
+ private void updateUserName() {
+ String userName = mUserNameInput.getText().toString();
+ // Disable the update button until the user name update has been done
+ mUpdateButton.setEnabled(false);
+ // Subscribe to updating the user name.
+ // Re-enable the button once the user name has been updated
+ mDisposable.add(mViewModel.updateUserName(userName)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> mUpdateButton.setEnabled(true),
+ throwable -> Log.e(TAG, "Unable to update username", throwable)));
+ }
+}
diff --git a/BasicRxJavaSample/app/src/main/java/com/example/android/observability/ui/UserViewModel.java b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/ui/UserViewModel.java
new file mode 100644
index 000000000..9a9733153
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/ui/UserViewModel.java
@@ -0,0 +1,71 @@
+/*
+ * 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.observability.ui;
+
+import android.arch.lifecycle.ViewModel;
+import com.example.android.observability.UserDataSource;
+import com.example.android.observability.persistence.User;
+import io.reactivex.Completable;
+import io.reactivex.Flowable;
+
+/**
+ * View Model for the {@link UserActivity}
+ */
+public class UserViewModel extends ViewModel {
+
+ private final UserDataSource mDataSource;
+
+ private User mUser;
+
+ public UserViewModel(UserDataSource dataSource) {
+ mDataSource = dataSource;
+ }
+
+ /**
+ * Get the user name of the user.
+ *
+ * @return a {@link Flowable} that will emit every time the user name has been updated.
+ */
+ public Flowable getUserName() {
+ return mDataSource.getUser()
+ // for every emission of the user, get the user name
+ .map(user -> {
+ mUser = user;
+ return user.getUserName();
+ });
+
+ }
+
+ /**
+ * Update the user name.
+ *
+ * @param userName the new user name
+ * @return a {@link Completable} that completes when the user name is updated
+ */
+ public Completable updateUserName(final String userName) {
+ return Completable.fromAction(() -> {
+ // if there's no use, create a new user.
+ // if we already have a user, then, since the user object is immutable,
+ // create a new user, with the id of the previous user and the updated user name.
+ mUser = mUser == null
+ ? new User(userName)
+ : new User(mUser.getId(), userName);
+
+ mDataSource.insertOrUpdateUser(mUser);
+ });
+ }
+}
diff --git a/BasicRxJavaSample/app/src/main/java/com/example/android/observability/ui/ViewModelFactory.java b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/ui/ViewModelFactory.java
new file mode 100644
index 000000000..87e512827
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/java/com/example/android/observability/ui/ViewModelFactory.java
@@ -0,0 +1,42 @@
+/*
+ * 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.observability.ui;
+
+import android.arch.lifecycle.ViewModel;
+import android.arch.lifecycle.ViewModelProvider;
+
+import com.example.android.observability.UserDataSource;
+
+/**
+ * Factory for ViewModels
+ */
+public class ViewModelFactory implements ViewModelProvider.Factory {
+
+ private final UserDataSource mDataSource;
+
+ public ViewModelFactory(UserDataSource dataSource) {
+ mDataSource = dataSource;
+ }
+
+ @Override
+ public T create(Class modelClass) {
+ if (modelClass.isAssignableFrom(UserViewModel.class)) {
+ return (T) new UserViewModel(mDataSource);
+ }
+ throw new IllegalArgumentException("Unknown ViewModel class");
+ }
+}
diff --git a/BasicRxJavaSample/app/src/main/res/layout/activity_user.xml b/BasicRxJavaSample/app/src/main/res/layout/activity_user.xml
new file mode 100644
index 000000000..c73ef3d24
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/res/layout/activity_user.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
diff --git a/BasicRxJavaSample/app/src/main/res/mipmap-hdpi/ic_launcher.png b/BasicRxJavaSample/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..550730310
Binary files /dev/null and b/BasicRxJavaSample/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/BasicRxJavaSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/BasicRxJavaSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..8fab6a3a5
Binary files /dev/null and b/BasicRxJavaSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/BasicRxJavaSample/app/src/main/res/mipmap-mdpi/ic_launcher.png b/BasicRxJavaSample/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..6bc7fcd6f
Binary files /dev/null and b/BasicRxJavaSample/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/BasicRxJavaSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/BasicRxJavaSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..1eecc0e7d
Binary files /dev/null and b/BasicRxJavaSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/BasicRxJavaSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/BasicRxJavaSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..ec87dcebe
Binary files /dev/null and b/BasicRxJavaSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/BasicRxJavaSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/BasicRxJavaSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..05ca079ca
Binary files /dev/null and b/BasicRxJavaSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/BasicRxJavaSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/BasicRxJavaSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..6f67f21ba
Binary files /dev/null and b/BasicRxJavaSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/BasicRxJavaSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/BasicRxJavaSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..8bac0f274
Binary files /dev/null and b/BasicRxJavaSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/BasicRxJavaSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/BasicRxJavaSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..0327e13fa
Binary files /dev/null and b/BasicRxJavaSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/BasicRxJavaSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/BasicRxJavaSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..bacd3e758
Binary files /dev/null and b/BasicRxJavaSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/BasicRxJavaSample/app/src/main/res/values/colors.xml b/BasicRxJavaSample/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..3ab3e9cbc
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/BasicRxJavaSample/app/src/main/res/values/strings.xml b/BasicRxJavaSample/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..f1d290bff
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ BasicRxJavaSample
+ Update username
+
diff --git a/BasicRxJavaSample/app/src/main/res/values/styles.xml b/BasicRxJavaSample/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..5885930df
--- /dev/null
+++ b/BasicRxJavaSample/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/BasicRxJavaSample/app/src/test/java/com/example/android/observability/UserViewModelTest.java b/BasicRxJavaSample/app/src/test/java/com/example/android/observability/UserViewModelTest.java
new file mode 100644
index 000000000..279a88298
--- /dev/null
+++ b/BasicRxJavaSample/app/src/test/java/com/example/android/observability/UserViewModelTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.observability;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.arch.core.executor.testing.InstantTaskExecutorRule;
+
+import com.example.android.observability.persistence.User;
+import com.example.android.observability.ui.UserViewModel;
+
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import io.reactivex.Flowable;
+
+/**
+ * Unit test for {@link UserViewModel}
+ */
+public class UserViewModelTest {
+
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Mock
+ private UserDataSource mDataSource;
+
+ @Captor
+ private ArgumentCaptor mUserArgumentCaptor;
+
+ private UserViewModel mViewModel;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mViewModel = new UserViewModel(mDataSource);
+ }
+
+ @Test
+ public void getUserName_whenNoUserSaved() throws InterruptedException {
+ // Given that the UserDataSource returns an empty list of users
+ when(mDataSource.getUser()).thenReturn(Flowable.empty());
+
+ //When getting the user name
+ mViewModel.getUserName()
+ .test()
+ // The user name is empty
+ .assertNoValues();
+ }
+
+ @Test
+ public void getUserName_whenUserSaved() throws InterruptedException {
+ // Given that the UserDataSource returns a user
+ User user = new User("user name");
+ when(mDataSource.getUser()).thenReturn(Flowable.just(user));
+
+ //When getting the user name
+ mViewModel.getUserName()
+ .test()
+ // The correct user name is emitted
+ .assertValue("user name");
+ }
+
+ @Test
+ public void updateUserName_updatesNameInDataSource() {
+ // When updating the user name
+ mViewModel.updateUserName("new user name")
+ .test()
+ .assertComplete();
+
+ // The user name is updated in the data source
+ verify(mDataSource).insertOrUpdateUser(mUserArgumentCaptor.capture());
+ assertThat(mUserArgumentCaptor.getValue().getUserName(), Matchers.is("new user name"));
+ }
+
+}
\ No newline at end of file
diff --git a/BasicRxJavaSample/build.gradle b/BasicRxJavaSample/build.gradle
new file mode 100644
index 000000000..43be627da
--- /dev/null
+++ b/BasicRxJavaSample/build.gradle
@@ -0,0 +1,20 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ apply from: 'versions.gradle'
+ addRepos(repositories)
+ dependencies {
+ classpath deps.android_gradle_plugin
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ addRepos(repositories)
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/BasicRxJavaSample/gradle.properties b/BasicRxJavaSample/gradle.properties
new file mode 100644
index 000000000..aac7c9b46
--- /dev/null
+++ b/BasicRxJavaSample/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/BasicRxJavaSample/gradle/wrapper/gradle-wrapper.jar b/BasicRxJavaSample/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..13372aef5
Binary files /dev/null and b/BasicRxJavaSample/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/BasicRxJavaSample/gradle/wrapper/gradle-wrapper.properties b/BasicRxJavaSample/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..e3716001d
--- /dev/null
+++ b/BasicRxJavaSample/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Jun 20 19:07:27 BST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/BasicRxJavaSample/gradlew b/BasicRxJavaSample/gradlew
new file mode 100755
index 000000000..9d82f7891
--- /dev/null
+++ b/BasicRxJavaSample/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/BasicRxJavaSample/gradlew.bat b/BasicRxJavaSample/gradlew.bat
new file mode 100644
index 000000000..aec99730b
--- /dev/null
+++ b/BasicRxJavaSample/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/BasicRxJavaSample/settings.gradle b/BasicRxJavaSample/settings.gradle
new file mode 100644
index 000000000..e7b4def49
--- /dev/null
+++ b/BasicRxJavaSample/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/BasicRxJavaSample/versions.gradle b/BasicRxJavaSample/versions.gradle
new file mode 100644
index 000000000..85822d50b
--- /dev/null
+++ b/BasicRxJavaSample/versions.gradle
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+
+
+/**
+* Shared file between builds so that they can all use the same dependencies and
+* maven repositories.
+**/
+ext.deps = [:]
+def versions = [:]
+versions.arch_core = "1.1.1"
+versions.room = "1.1.0-beta2"
+versions.lifecycle = "1.1.1"
+versions.support = "27.1.0"
+versions.dagger = "2.11"
+versions.junit = "4.12"
+versions.espresso = "3.0.1"
+versions.retrofit = "2.3.0"
+versions.okhttp_logging_interceptor = "3.9.0"
+versions.mockwebserver = "3.8.1"
+versions.apache_commons = "2.5"
+versions.mockito = "2.7.19"
+versions.mockito_all = "1.10.19"
+versions.dexmaker = "2.2.0"
+versions.constraint_layout = "1.0.2"
+versions.glide = "3.8.0"
+versions.timber = "4.5.1"
+versions.android_gradle_plugin = "3.0.1"
+versions.rxjava2 = "2.1.3"
+versions.rx_android = "2.0.1"
+versions.atsl_runner = "1.0.1"
+versions.atsl_rules = "1.0.1"
+versions.hamcrest = "1.3"
+versions.kotlin = "1.2.20"
+versions.paging = "1.0.0-beta1"
+def deps = [:]
+
+def support = [:]
+support.annotations = "com.android.support:support-annotations:$versions.support"
+support.app_compat = "com.android.support:appcompat-v7:$versions.support"
+support.recyclerview = "com.android.support:recyclerview-v7:$versions.support"
+support.cardview = "com.android.support:cardview-v7:$versions.support"
+support.design = "com.android.support:design:$versions.support"
+support.v4 = "com.android.support:support-v4:$versions.support"
+support.core_utils = "com.android.support:support-core-utils:$versions.support"
+deps.support = support
+
+def room = [:]
+room.runtime = "android.arch.persistence.room:runtime:$versions.room"
+room.compiler = "android.arch.persistence.room:compiler:$versions.room"
+room.rxjava2 = "android.arch.persistence.room:rxjava2:$versions.room"
+room.testing = "android.arch.persistence.room:testing:$versions.room"
+deps.room = room
+
+def lifecycle = [:]
+lifecycle.runtime = "android.arch.lifecycle:runtime:$versions.lifecycle"
+lifecycle.extensions = "android.arch.lifecycle:extensions:$versions.lifecycle"
+lifecycle.java8 = "android.arch.lifecycle:common-java8:$versions.lifecycle"
+lifecycle.compiler = "android.arch.lifecycle:compiler:$versions.lifecycle"
+deps.lifecycle = lifecycle
+
+def arch_core = [:]
+arch_core.testing = "android.arch.core:core-testing:$versions.arch_core"
+deps.arch_core = arch_core
+
+def retrofit = [:]
+retrofit.runtime = "com.squareup.retrofit2:retrofit:$versions.retrofit"
+retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit"
+retrofit.mock = "com.squareup.retrofit2:retrofit-mock:$versions.retrofit"
+deps.retrofit = retrofit
+deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:${versions.okhttp_logging_interceptor}"
+
+def dagger = [:]
+dagger.runtime = "com.google.dagger:dagger:$versions.dagger"
+dagger.android = "com.google.dagger:dagger-android:$versions.dagger"
+dagger.android_support = "com.google.dagger:dagger-android-support:$versions.dagger"
+dagger.compiler = "com.google.dagger:dagger-compiler:$versions.dagger"
+dagger.android_support_compiler = "com.google.dagger:dagger-android-processor:$versions.dagger"
+
+deps.dagger = dagger
+
+def espresso = [:]
+espresso.core = "com.android.support.test.espresso:espresso-core:$versions.espresso"
+espresso.contrib = "com.android.support.test.espresso:espresso-contrib:$versions.espresso"
+espresso.intents = "com.android.support.test.espresso:espresso-intents:$versions.espresso"
+deps.espresso = espresso
+
+def atsl = [:]
+atsl.runner = "com.android.support.test:runner:$versions.atsl_runner"
+atsl.rules = "com.android.support.test:rules:$versions.atsl_runner"
+deps.atsl = atsl
+
+def mockito = [:]
+mockito.core = "org.mockito:mockito-core:$versions.mockito"
+mockito.all = "org.mockito:mockito-all:$versions.mockito_all"
+deps.mockito = mockito
+
+def kotlin = [:]
+kotlin.stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jre7:$versions.kotlin"
+kotlin.test = "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin"
+kotlin.plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
+
+deps.kotlin = kotlin
+deps.paging = "android.arch.paging:runtime:$versions.paging"
+deps.glide = "com.github.bumptech.glide:glide:$versions.glide"
+deps.dexmaker = "com.linkedin.dexmaker:dexmaker-mockito:$versions.dexmaker"
+deps.constraint_layout = "com.android.support.constraint:constraint-layout:$versions.constraint_layout"
+deps.timber = "com.jakewharton.timber:timber:$versions.timber"
+deps.junit = "junit:junit:$versions.junit"
+deps.mock_web_server = "com.squareup.okhttp3:mockwebserver:$versions.mockwebserver"
+deps.rxjava2 = "io.reactivex.rxjava2:rxjava:$versions.rxjava2"
+deps.rx_android = "io.reactivex.rxjava2:rxandroid:$versions.rx_android"
+deps.hamcrest = "org.hamcrest:hamcrest-all:$versions.hamcrest"
+deps.android_gradle_plugin = "com.android.tools.build:gradle:$versions.android_gradle_plugin"
+ext.deps = deps
+
+def build_versions = [:]
+build_versions.min_sdk = 14
+build_versions.target_sdk = 26
+build_versions.build_tools = "27.0.3"
+ext.build_versions = build_versions
+
+
+def addRepos(RepositoryHandler handler) {
+ handler.google()
+ handler.jcenter()
+ handler.maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
+}
+ext.addRepos = this.&addRepos
diff --git a/BasicRxJavaSampleKotlin/.gitignore b/BasicRxJavaSampleKotlin/.gitignore
new file mode 100644
index 000000000..46888caac
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/.gitignore
@@ -0,0 +1,11 @@
+*.iml
+.gradle
+/local.properties
+.idea
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.idea/
diff --git a/BasicRxJavaSampleKotlin/.google/packaging.yaml b/BasicRxJavaSampleKotlin/.google/packaging.yaml
new file mode 100644
index 000000000..dc2bc945d
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/.google/packaging.yaml
@@ -0,0 +1,41 @@
+#
+# 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.
+
+# GOOGLE SAMPLE PACKAGING DATA
+#
+# This file is used by Google as part of our samples packaging process.
+# End users may safely ignore this file. It has no relevance to other systems.
+---
+# Values: {DRAFT | PUBLISHED | INTERNAL | DEPRECATED | SUPERCEDED}
+status: PUBLISHED
+
+# Optional, put additional explanation here for DEPRECATED or SUPERCEDED.
+# statusNote:
+
+# See http://go/sample-categories
+technologies: [Android]
+categories: [Architecture]
+languages: [Kotlin]
+solutions: [Mobile]
+
+# May be omitted if unpublished
+github: googlesamples/android-architecture-components
+
+# Values: BEGINNER | INTERMEDIATE | ADVANCED | EXPERT
+level: ADVANCED
+
+# Default: apache2. May be omitted for most samples.
+# Alternatives: apache2-android (for AOSP)
+license: apache2
diff --git a/BasicRxJavaSampleKotlin/README.md b/BasicRxJavaSampleKotlin/README.md
new file mode 100644
index 000000000..f5c0f43a4
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/README.md
@@ -0,0 +1,52 @@
+Room & RxJava (Kotlin) Sample
+============================
+
+This is an API sample to showcase how to use [Room](https://developer.android.com/topic/libraries/architecture/room.html),
+with RxJava's [Flowable](http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Flowable.html) objects in Kotlin.
+
+Introduction
+-------------
+
+### Functionality
+The sample app shows an editable user name, stored in the database.
+
+### Implementation
+
+#### Data layer
+
+The database is created using Room and has one entity: a `User`. Room generates the corresponding SQLite table at
+runtime.
+
+Queries are executed in the `UserDao` class. The user retrieval is done via an observable query implemented using a
+`Flowable`. Every time the user data is updated, the Flowable object will emit automatically, allowing to update the UI
+based on the latest data. The Flowable will emit only when the query result contains at least a row. When there is no
+data to match the query, the Flowable will not emit.
+
+#### Presentation layer
+
+The app has a main Activity that displays the data.
+The Activity works with a ViewModel to do the following:
+* subscribe to the emissions of the user name and updates the UI every time there is a new user name emitted
+* notify the ViewModel when the pressed the "Update" and passes the new user name.
+The ViewModel works with the data source to get and save the data.
+
+Room guarantees that the observable query will be triggered on a background thread. In the Activity, the Flowable events
+are set to be received on the main thread, so the UI can be updated. The insert query is synchronous so it's wrapped in
+a Completable and executed on a background thread. On completion, the Activity is notified on the main thread.
+
+License
+--------
+
+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.
diff --git a/BasicRxJavaSampleKotlin/app/.gitignore b/BasicRxJavaSampleKotlin/app/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/BasicRxJavaSampleKotlin/app/build.gradle b/BasicRxJavaSampleKotlin/app/build.gradle
new file mode 100644
index 000000000..84b89d268
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/build.gradle
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-kapt'
+apply plugin: 'kotlin-android-extensions'
+
+android {
+ compileSdkVersion build_versions.target_sdk
+ buildToolsVersion build_versions.build_tools
+
+ defaultConfig {
+ applicationId "com.example.android.observability"
+ minSdkVersion build_versions.min_sdk
+ targetSdkVersion build_versions.target_sdk
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ // Support libraries
+ implementation deps.support.app_compat
+ implementation deps.support.v4
+ implementation deps.support.design
+
+ // Architecture components
+ implementation deps.lifecycle.runtime
+ implementation deps.lifecycle.extensions
+ kapt deps.lifecycle.compiler
+ implementation deps.room.runtime
+ kapt deps.room.compiler
+ implementation deps.room.rxjava2
+
+ // RxJava
+ implementation deps.rx_android
+ implementation deps.rxjava2
+
+ // kotlin
+ // Kotlin
+ implementation deps.kotlin.stdlib
+
+ // Dependencies for local unit tests
+ testImplementation deps.junit
+ testImplementation deps.mockito.all
+ testImplementation deps.hamcrest
+ testImplementation deps.arch_core.testing
+ testImplementation deps.kotlin.stdlib
+ testImplementation deps.kotlin.test
+
+ // Android Testing Support Library's runner and rules
+ androidTestCompile deps.atsl.runner
+ androidTestCompile deps.atsl.rules
+ androidTestCompile deps.room.testing
+ androidTestCompile deps.arch_core.testing
+
+ // Dependencies for Android unit tests
+ androidTestCompile deps.junit
+ androidTestCompile deps.mockito.core, { exclude group: 'net.bytebuddy' }
+ androidTestCompile deps.dexmaker
+
+ // Espresso UI Testing
+ androidTestCompile deps.espresso.core
+ androidTestCompile deps.espresso.contrib
+ androidTestCompile deps.espresso.intents
+
+ // Resolve conflicts between main and test APK:
+ androidTestCompile deps.support.annotations
+ androidTestCompile deps.support.v4
+ androidTestCompile deps.support.app_compat
+ androidTestCompile deps.support.design
+}
diff --git a/BasicRxJavaSampleKotlin/app/proguard-rules.pro b/BasicRxJavaSampleKotlin/app/proguard-rules.pro
new file mode 100644
index 000000000..5e78d02af
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/florinam/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/BasicRxJavaSampleKotlin/app/src/androidTest/java/com/example/android/observability/persistence/UserDaoTest.kt b/BasicRxJavaSampleKotlin/app/src/androidTest/java/com/example/android/observability/persistence/UserDaoTest.kt
new file mode 100644
index 000000000..8ba996874
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/androidTest/java/com/example/android/observability/persistence/UserDaoTest.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.observability.persistence
+
+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 org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Test the implementation of [UserDao]
+ */
+@RunWith(AndroidJUnit4::class)
+class UserDaoTest {
+
+ @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ private lateinit var database: UsersDatabase
+
+ @Before fun initDb() {
+ // using an in-memory database because the information stored here disappears after test
+ database = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
+ UsersDatabase::class.java)
+ // allowing main thread queries, just for testing
+ .allowMainThreadQueries()
+ .build()
+ }
+
+ @After fun closeDb() {
+ database.close()
+ }
+
+ @Test fun getUsersWhenNoUserInserted() {
+ database.userDao().getUserById("123")
+ .test()
+ .assertNoValues()
+ }
+
+ @Test fun insertAndGetUser() {
+ // When inserting a new user in the data source
+ database.userDao().insertUser(USER)
+
+ // When subscribing to the emissions of the user
+ database.userDao().getUserById(USER.id)
+ .test()
+ // assertValue asserts that there was only one emission of the user
+ .assertValue { it.id == USER.id && it.userName == USER.userName }
+ }
+
+ @Test fun updateAndGetUser() {
+ // Given that we have a user in the data source
+ database.userDao().insertUser(USER)
+
+ // When we are updating the name of the user
+ val updatedUser = User(USER.id, "new username")
+ database.userDao().insertUser(updatedUser)
+
+ // When subscribing to the emissions of the user
+ database.userDao().getUserById(USER.id)
+ .test()
+ // assertValue asserts that there was only one emission of the user
+ .assertValue { it.id == USER.id && it.userName == "new username" }
+ }
+
+ @Test fun deleteAndGetUser() {
+ // Given that we have a user in the data source
+ database.userDao().insertUser(USER)
+
+ //When we are deleting all users
+ database.userDao().deleteAllUsers()
+ // When subscribing to the emissions of the user
+ database.userDao().getUserById(USER.id)
+ .test()
+ // check that there's no user emitted
+ .assertNoValues()
+ }
+
+ companion object {
+ private val USER = User("id", "username")
+ }
+}
diff --git a/BasicRxJavaSampleKotlin/app/src/main/AndroidManifest.xml b/BasicRxJavaSampleKotlin/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..4ca0db523
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/main/AndroidManifest.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/Injection.kt b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/Injection.kt
new file mode 100644
index 000000000..69333ff9d
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/Injection.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.observability
+
+import android.content.Context
+import com.example.android.observability.persistence.UserDao
+
+import com.example.android.observability.persistence.UsersDatabase
+import com.example.android.observability.ui.ViewModelFactory
+
+/**
+ * Enables injection of data sources.
+ */
+object Injection {
+
+ fun provideUserDataSource(context: Context): UserDao {
+ val database = UsersDatabase.getInstance(context)
+ return database.userDao()
+ }
+
+ fun provideViewModelFactory(context: Context): ViewModelFactory {
+ val dataSource = provideUserDataSource(context)
+ return ViewModelFactory(dataSource)
+ }
+}
diff --git a/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/persistence/User.kt b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/persistence/User.kt
new file mode 100644
index 000000000..dc657ae86
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/persistence/User.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.observability.persistence
+
+import android.arch.persistence.room.ColumnInfo
+import android.arch.persistence.room.Entity
+import android.arch.persistence.room.PrimaryKey
+import java.util.*
+
+@Entity(tableName = "users")
+data class User(@PrimaryKey
+ @ColumnInfo(name = "userid")
+ val id: String = UUID.randomUUID().toString(),
+ @ColumnInfo(name = "username")
+ val userName: String)
\ No newline at end of file
diff --git a/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/persistence/UserDao.kt b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/persistence/UserDao.kt
new file mode 100644
index 000000000..a5770a0eb
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/persistence/UserDao.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.observability.persistence
+
+import android.arch.persistence.room.Dao
+import android.arch.persistence.room.Insert
+import android.arch.persistence.room.OnConflictStrategy
+import android.arch.persistence.room.Query
+
+import io.reactivex.Flowable
+
+/**
+ * Data Access Object for the users table.
+ */
+@Dao
+interface UserDao {
+
+ /**
+ * Get a user by id.
+
+ * @return the user from the table with a specific id.
+ */
+ @Query("SELECT * FROM Users WHERE userid = :id")
+ fun getUserById(id: String): Flowable
+
+ /**
+ * Insert a user in the database. If the user already exists, replace it.
+
+ * @param user the user to be inserted.
+ */
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertUser(user: User)
+
+ /**
+ * Delete all users.
+ */
+ @Query("DELETE FROM Users")
+ fun deleteAllUsers()
+}
diff --git a/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/persistence/UsersDatabase.kt b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/persistence/UsersDatabase.kt
new file mode 100644
index 000000000..7f3ece204
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/persistence/UsersDatabase.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.observability.persistence
+
+import android.arch.persistence.room.Database
+import android.arch.persistence.room.Room
+import android.arch.persistence.room.RoomDatabase
+import android.content.Context
+
+/**
+ * The Room database that contains the Users table
+ */
+@Database(entities = arrayOf(User::class), version = 1)
+abstract class UsersDatabase : RoomDatabase() {
+
+ abstract fun userDao(): UserDao
+
+ companion object {
+
+ @Volatile private var INSTANCE: UsersDatabase? = null
+
+ fun getInstance(context: Context): UsersDatabase =
+ INSTANCE ?: synchronized(this) {
+ INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
+ }
+
+ private fun buildDatabase(context: Context) =
+ Room.databaseBuilder(context.applicationContext,
+ UsersDatabase::class.java, "Sample.db")
+ .build()
+ }
+}
diff --git a/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/ui/UserActivity.kt b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/ui/UserActivity.kt
new file mode 100644
index 000000000..6efa835d5
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/ui/UserActivity.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.observability.ui
+
+import android.arch.lifecycle.ViewModelProviders
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+import android.util.Log
+import com.example.android.observability.Injection
+import com.example.android.observability.R
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.schedulers.Schedulers
+import kotlinx.android.synthetic.main.activity_user.*
+
+/**
+ * Main screen of the app. Displays a user name and gives the option to update the user name.
+ */
+class UserActivity : AppCompatActivity() {
+
+ private lateinit var viewModelFactory: ViewModelFactory
+
+ private lateinit var viewModel: UserViewModel
+
+ private val disposable = CompositeDisposable()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_user)
+
+ viewModelFactory = Injection.provideViewModelFactory(this)
+ viewModel = ViewModelProviders.of(this, viewModelFactory).get(UserViewModel::class.java)
+ update_user_button.setOnClickListener { updateUserName() }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ // Subscribe to the emissions of the user name from the view model.
+ // Update the user name text view, at every onNext emission.
+ // In case of error, log the exception.
+ disposable.add(viewModel.userName()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe({ this.user_name.text = it },
+ { error -> Log.e(TAG, "Unable to get username", error) }))
+ }
+
+ override fun onStop() {
+ super.onStop()
+
+ // clear all the subscription
+ disposable.clear()
+ }
+
+ private fun updateUserName() {
+ val userName = user_name_input.text.toString()
+ // Disable the update button until the user name update has been done
+ update_user_button.isEnabled = false
+ // Subscribe to updating the user name.
+ // Enable back the button once the user name has been updated
+ disposable.add(viewModel.updateUserName(userName)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe({ update_user_button.isEnabled = true },
+ { error -> Log.e(TAG, "Unable to update username", error) }))
+ }
+
+ companion object {
+ private val TAG = UserActivity::class.java.simpleName
+ }
+}
diff --git a/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/ui/UserViewModel.kt b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/ui/UserViewModel.kt
new file mode 100644
index 000000000..3bdfa3a76
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/ui/UserViewModel.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.observability.ui
+
+import android.arch.lifecycle.ViewModel
+import com.example.android.observability.persistence.User
+import com.example.android.observability.persistence.UserDao
+import io.reactivex.Completable
+import io.reactivex.Flowable
+
+/**
+ * View Model for the [UserActivity]
+ */
+class UserViewModel(private val dataSource: UserDao) : ViewModel() {
+
+ /**
+ * Get the user name of the user.
+
+ * @return a [Flowable] that will emit every time the user name has been updated.
+ */
+ // for every emission of the user, get the user name
+ fun userName(): Flowable {
+ return dataSource.getUserById(USER_ID)
+ .map { user -> user.userName }
+ }
+
+ /**
+ * Update the user name.
+ * @param userName the new user name
+ * *
+ * @return a [Completable] that completes when the user name is updated
+ */
+ fun updateUserName(userName: String): Completable {
+ return Completable.fromAction {
+ val user = User(USER_ID, userName)
+ dataSource.insertUser(user)
+ }
+ }
+
+ companion object {
+ // using a hardcoded value for simplicity
+ const val USER_ID = "1"
+ }
+}
diff --git a/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/ui/ViewModelFactory.kt b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/ui/ViewModelFactory.kt
new file mode 100644
index 000000000..d51db7114
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/ui/ViewModelFactory.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.observability.ui
+
+import android.arch.lifecycle.ViewModel
+import android.arch.lifecycle.ViewModelProvider
+import com.example.android.observability.persistence.UserDao
+
+/**
+ * Factory for ViewModels
+ */
+class ViewModelFactory(private val dataSource: UserDao) : ViewModelProvider.Factory {
+
+ override fun create(modelClass: Class): T {
+ if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
+ return UserViewModel(dataSource) as T
+ }
+ throw IllegalArgumentException("Unknown ViewModel class")
+ }
+}
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/layout/activity_user.xml b/BasicRxJavaSampleKotlin/app/src/main/res/layout/activity_user.xml
new file mode 100644
index 000000000..52923f5a7
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/main/res/layout/activity_user.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-hdpi/ic_launcher.png b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..550730310
Binary files /dev/null and b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..8fab6a3a5
Binary files /dev/null and b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-mdpi/ic_launcher.png b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..6bc7fcd6f
Binary files /dev/null and b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..1eecc0e7d
Binary files /dev/null and b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..ec87dcebe
Binary files /dev/null and b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..05ca079ca
Binary files /dev/null and b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..6f67f21ba
Binary files /dev/null and b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..8bac0f274
Binary files /dev/null and b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..0327e13fa
Binary files /dev/null and b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..bacd3e758
Binary files /dev/null and b/BasicRxJavaSampleKotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/values/colors.xml b/BasicRxJavaSampleKotlin/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..3ab3e9cbc
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/values/strings.xml b/BasicRxJavaSampleKotlin/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..4b6dac5c3
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ BasicRxJavaSampleKotlin
+ Update username
+
diff --git a/BasicRxJavaSampleKotlin/app/src/main/res/values/styles.xml b/BasicRxJavaSampleKotlin/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..5885930df
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/BasicRxJavaSampleKotlin/app/src/test/java/com/example/android/observability/MockitoKotlinHelpers.kt b/BasicRxJavaSampleKotlin/app/src/test/java/com/example/android/observability/MockitoKotlinHelpers.kt
new file mode 100644
index 000000000..d4d294674
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/test/java/com/example/android/observability/MockitoKotlinHelpers.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.observability
+
+import org.mockito.ArgumentCaptor
+
+/**
+ * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException
+ * when null is returned.
+ */
+fun capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture()
\ No newline at end of file
diff --git a/BasicRxJavaSampleKotlin/app/src/test/java/com/example/android/observability/UserViewModelTest.kt b/BasicRxJavaSampleKotlin/app/src/test/java/com/example/android/observability/UserViewModelTest.kt
new file mode 100644
index 000000000..4b16c35aa
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/app/src/test/java/com/example/android/observability/UserViewModelTest.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.observability
+
+import android.arch.core.executor.testing.InstantTaskExecutorRule
+import com.example.android.observability.persistence.User
+import com.example.android.observability.persistence.UserDao
+import com.example.android.observability.ui.UserViewModel
+import io.reactivex.Flowable
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Unit test for [UserViewModel]
+ */
+class UserViewModelTest {
+
+ @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Mock private lateinit var dataSource: UserDao
+
+ @Captor private lateinit var userArgumentCaptor: ArgumentCaptor
+
+ private lateinit var viewModel: UserViewModel
+
+ @Before fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ viewModel = UserViewModel(dataSource)
+ }
+
+ @Test fun getUserName_whenNoUserSaved() {
+ // Given that the UserDataSource returns an empty list of users
+ `when`(dataSource.getUserById(UserViewModel.USER_ID)).thenReturn(Flowable.empty())
+
+ //When getting the user name
+ viewModel.userName()
+ .test()
+ // The user name is empty
+ .assertNoValues()
+ }
+
+ @Test fun getUserName_whenUserSaved() {
+ // Given that the UserDataSource returns a user
+ val user = User(userName = "user name")
+ `when`(dataSource.getUserById(UserViewModel.USER_ID)).thenReturn(Flowable.just(user))
+
+ //When getting the user name
+ viewModel.userName()
+ .test()
+ // The correct user name is emitted
+ .assertValue("user name")
+ }
+
+ @Test fun updateUserName_updatesNameInDataSource() {
+ // When updating the user name
+ viewModel.updateUserName("new user name")
+ .test()
+ .assertComplete()
+
+ // The user name is updated in the data source
+ // using ?: User("someUser") because otherwise, we get
+ // "IllegalStateException: userArgumentCaptor.capture() must not be null"
+ verify(dataSource).insertUser(capture(userArgumentCaptor))
+ assertThat(userArgumentCaptor.value.userName, Matchers.`is`("new user name"))
+ }
+
+}
\ No newline at end of file
diff --git a/BasicRxJavaSampleKotlin/build.gradle b/BasicRxJavaSampleKotlin/build.gradle
new file mode 100644
index 000000000..69f1ada2c
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/build.gradle
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ apply from: 'versions.gradle'
+ addRepos(repositories)
+ dependencies {
+ classpath deps.android_gradle_plugin
+ classpath deps.kotlin.plugin
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ addRepos(repositories)
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
+
+// Define versions in a single place
+ext {
+ // Sdk and tools
+ minSdkVersion = 15
+ targetSdkVersion = 26
+ compileSdkVersion = 26
+ buildToolsVersion = '26.0.1'
+
+ // App dependencies
+ supportLibraryVersion = '26.0.2'
+ junitVersion = '4.12'
+ mockitoVersion = '1.10.19'
+ hamcrestVersion = '1.3'
+ runnerVersion = '1.0.1'
+ rulesVersion = '1.0.1'
+ espressoVersion = '3.0.1'
+ architectureComponentsVersion = "1.0.0-alpha9"
+ rxjavaVersion = "2.1.3"
+ rxandroidVersion = "2.0.1"
+}
diff --git a/BasicRxJavaSampleKotlin/gradle.properties b/BasicRxJavaSampleKotlin/gradle.properties
new file mode 100644
index 000000000..aac7c9b46
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/BasicRxJavaSampleKotlin/gradle/wrapper/gradle-wrapper.jar b/BasicRxJavaSampleKotlin/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..13372aef5
Binary files /dev/null and b/BasicRxJavaSampleKotlin/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/BasicRxJavaSampleKotlin/gradle/wrapper/gradle-wrapper.properties b/BasicRxJavaSampleKotlin/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..d8eee821f
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Aug 02 20:57:40 BST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/BasicRxJavaSampleKotlin/gradlew b/BasicRxJavaSampleKotlin/gradlew
new file mode 100755
index 000000000..9d82f7891
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/BasicRxJavaSampleKotlin/gradlew.bat b/BasicRxJavaSampleKotlin/gradlew.bat
new file mode 100644
index 000000000..aec99730b
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/BasicRxJavaSampleKotlin/settings.gradle b/BasicRxJavaSampleKotlin/settings.gradle
new file mode 100644
index 000000000..e7b4def49
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/BasicRxJavaSampleKotlin/versions.gradle b/BasicRxJavaSampleKotlin/versions.gradle
new file mode 100644
index 000000000..85822d50b
--- /dev/null
+++ b/BasicRxJavaSampleKotlin/versions.gradle
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+
+
+/**
+* Shared file between builds so that they can all use the same dependencies and
+* maven repositories.
+**/
+ext.deps = [:]
+def versions = [:]
+versions.arch_core = "1.1.1"
+versions.room = "1.1.0-beta2"
+versions.lifecycle = "1.1.1"
+versions.support = "27.1.0"
+versions.dagger = "2.11"
+versions.junit = "4.12"
+versions.espresso = "3.0.1"
+versions.retrofit = "2.3.0"
+versions.okhttp_logging_interceptor = "3.9.0"
+versions.mockwebserver = "3.8.1"
+versions.apache_commons = "2.5"
+versions.mockito = "2.7.19"
+versions.mockito_all = "1.10.19"
+versions.dexmaker = "2.2.0"
+versions.constraint_layout = "1.0.2"
+versions.glide = "3.8.0"
+versions.timber = "4.5.1"
+versions.android_gradle_plugin = "3.0.1"
+versions.rxjava2 = "2.1.3"
+versions.rx_android = "2.0.1"
+versions.atsl_runner = "1.0.1"
+versions.atsl_rules = "1.0.1"
+versions.hamcrest = "1.3"
+versions.kotlin = "1.2.20"
+versions.paging = "1.0.0-beta1"
+def deps = [:]
+
+def support = [:]
+support.annotations = "com.android.support:support-annotations:$versions.support"
+support.app_compat = "com.android.support:appcompat-v7:$versions.support"
+support.recyclerview = "com.android.support:recyclerview-v7:$versions.support"
+support.cardview = "com.android.support:cardview-v7:$versions.support"
+support.design = "com.android.support:design:$versions.support"
+support.v4 = "com.android.support:support-v4:$versions.support"
+support.core_utils = "com.android.support:support-core-utils:$versions.support"
+deps.support = support
+
+def room = [:]
+room.runtime = "android.arch.persistence.room:runtime:$versions.room"
+room.compiler = "android.arch.persistence.room:compiler:$versions.room"
+room.rxjava2 = "android.arch.persistence.room:rxjava2:$versions.room"
+room.testing = "android.arch.persistence.room:testing:$versions.room"
+deps.room = room
+
+def lifecycle = [:]
+lifecycle.runtime = "android.arch.lifecycle:runtime:$versions.lifecycle"
+lifecycle.extensions = "android.arch.lifecycle:extensions:$versions.lifecycle"
+lifecycle.java8 = "android.arch.lifecycle:common-java8:$versions.lifecycle"
+lifecycle.compiler = "android.arch.lifecycle:compiler:$versions.lifecycle"
+deps.lifecycle = lifecycle
+
+def arch_core = [:]
+arch_core.testing = "android.arch.core:core-testing:$versions.arch_core"
+deps.arch_core = arch_core
+
+def retrofit = [:]
+retrofit.runtime = "com.squareup.retrofit2:retrofit:$versions.retrofit"
+retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit"
+retrofit.mock = "com.squareup.retrofit2:retrofit-mock:$versions.retrofit"
+deps.retrofit = retrofit
+deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:${versions.okhttp_logging_interceptor}"
+
+def dagger = [:]
+dagger.runtime = "com.google.dagger:dagger:$versions.dagger"
+dagger.android = "com.google.dagger:dagger-android:$versions.dagger"
+dagger.android_support = "com.google.dagger:dagger-android-support:$versions.dagger"
+dagger.compiler = "com.google.dagger:dagger-compiler:$versions.dagger"
+dagger.android_support_compiler = "com.google.dagger:dagger-android-processor:$versions.dagger"
+
+deps.dagger = dagger
+
+def espresso = [:]
+espresso.core = "com.android.support.test.espresso:espresso-core:$versions.espresso"
+espresso.contrib = "com.android.support.test.espresso:espresso-contrib:$versions.espresso"
+espresso.intents = "com.android.support.test.espresso:espresso-intents:$versions.espresso"
+deps.espresso = espresso
+
+def atsl = [:]
+atsl.runner = "com.android.support.test:runner:$versions.atsl_runner"
+atsl.rules = "com.android.support.test:rules:$versions.atsl_runner"
+deps.atsl = atsl
+
+def mockito = [:]
+mockito.core = "org.mockito:mockito-core:$versions.mockito"
+mockito.all = "org.mockito:mockito-all:$versions.mockito_all"
+deps.mockito = mockito
+
+def kotlin = [:]
+kotlin.stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jre7:$versions.kotlin"
+kotlin.test = "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin"
+kotlin.plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
+
+deps.kotlin = kotlin
+deps.paging = "android.arch.paging:runtime:$versions.paging"
+deps.glide = "com.github.bumptech.glide:glide:$versions.glide"
+deps.dexmaker = "com.linkedin.dexmaker:dexmaker-mockito:$versions.dexmaker"
+deps.constraint_layout = "com.android.support.constraint:constraint-layout:$versions.constraint_layout"
+deps.timber = "com.jakewharton.timber:timber:$versions.timber"
+deps.junit = "junit:junit:$versions.junit"
+deps.mock_web_server = "com.squareup.okhttp3:mockwebserver:$versions.mockwebserver"
+deps.rxjava2 = "io.reactivex.rxjava2:rxjava:$versions.rxjava2"
+deps.rx_android = "io.reactivex.rxjava2:rxandroid:$versions.rx_android"
+deps.hamcrest = "org.hamcrest:hamcrest-all:$versions.hamcrest"
+deps.android_gradle_plugin = "com.android.tools.build:gradle:$versions.android_gradle_plugin"
+ext.deps = deps
+
+def build_versions = [:]
+build_versions.min_sdk = 14
+build_versions.target_sdk = 26
+build_versions.build_tools = "27.0.3"
+ext.build_versions = build_versions
+
+
+def addRepos(RepositoryHandler handler) {
+ handler.google()
+ handler.jcenter()
+ handler.maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
+}
+ext.addRepos = this.&addRepos
diff --git a/BasicSample/.google/packaging.yaml b/BasicSample/.google/packaging.yaml
new file mode 100644
index 000000000..108903dd1
--- /dev/null
+++ b/BasicSample/.google/packaging.yaml
@@ -0,0 +1,41 @@
+#
+# 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.
+
+# GOOGLE SAMPLE PACKAGING DATA
+#
+# This file is used by Google as part of our samples packaging process.
+# End users may safely ignore this file. It has no relevance to other systems.
+---
+# Values: {DRAFT | PUBLISHED | INTERNAL | DEPRECATED | SUPERCEDED}
+status: PUBLISHED
+
+# Optional, put additional explanation here for DEPRECATED or SUPERCEDED.
+# statusNote:
+
+# See http://go/sample-categories
+technologies: [Android]
+categories: [Architecture]
+languages: [Java]
+solutions: [Mobile]
+
+# May be omitted if unpublished
+github: googlesamples/android-architecture-components
+
+# Values: BEGINNER | INTERMEDIATE | ADVANCED | EXPERT
+level: ADVANCED
+
+# Default: apache2. May be omitted for most samples.
+# Alternatives: apache2-android (for AOSP)
+license: apache2
diff --git a/BasicSample/README.md b/BasicSample/README.md
index 6871dfa47..f25fc5d2c 100644
--- a/BasicSample/README.md
+++ b/BasicSample/README.md
@@ -1,5 +1,5 @@
Android Architecture Components Basic Sample
-===================================
+=============================================
This sample showcases the following Architecture Components:
@@ -7,14 +7,17 @@ This sample showcases the following Architecture Components:
* [ViewModels](https://developer.android.com/reference/android/arch/lifecycle/ViewModel.html)
* [LiveData](https://developer.android.com/reference/android/arch/lifecycle/LiveData.html)
-## Features
+Introduction
+-------------
+
+### Features
This sample contains two screens: a list of products and a detail view, that shows product reviews.
-### Presentation layer
+#### 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.
@@ -46,55 +49,29 @@ The app uses a Model-View-ViewModel (MVVM) architecture for the presentation lay
});
```
-### Data layer
+#### Data layer
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 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>>() {
- @Override
- public LiveData> 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:
-
-
-
-
-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
--------
+License
+--------
Copyright 2015 The Android Open Source Project, Inc.
diff --git a/BasicSample/app/build.gradle b/BasicSample/app/build.gradle
index 79eb18434..2defb4e2c 100644
--- a/BasicSample/app/build.gradle
+++ b/BasicSample/app/build.gradle
@@ -17,12 +17,13 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 25
- buildToolsVersion rootProject.buildToolsVersion
+ compileSdkVersion build_versions.target_sdk
+ buildToolsVersion build_versions.build_tools
+
defaultConfig {
applicationId 'com.example.android.persistence'
- minSdkVersion 21
- targetSdkVersion 25
+ minSdkVersion build_versions.min_sdk
+ targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@@ -44,33 +45,40 @@ android {
abortOnError false
}
+ compileOptions {
+ targetCompatibility 1.8
+ sourceCompatibility 1.8
+ }
}
dependencies {
- compile fileTree(include: ['*.jar'], dir: 'libs')
- compile 'com.android.support:appcompat-v7:' + rootProject.supportLibVersion;
- compile 'com.android.support:cardview-v7:' + rootProject.supportLibVersion;
- compile 'com.android.support:recyclerview-v7:' + rootProject.supportLibVersion;
- compile 'android.arch.lifecycle:extensions:' + rootProject.archLifecycleVersion;
- compile 'android.arch.persistence.room:runtime:' + rootProject.archRoomVersion;
- annotationProcessor "android.arch.lifecycle:compiler:" + rootProject.archLifecycleVersion;
- annotationProcessor "android.arch.persistence.room:compiler:" + rootProject.archRoomVersion;
+ // Support libraries
+ implementation deps.support.app_compat
+ implementation deps.support.v4
+ implementation deps.support.design
+ implementation deps.support.cardview
- testCompile 'junit:junit:4.12'
+ // Architecture components
+ implementation deps.lifecycle.runtime
+ implementation deps.lifecycle.extensions
+ annotationProcessor deps.lifecycle.compiler
+ implementation deps.room.runtime
+ annotationProcessor deps.room.compiler
- // Testing-only dependencies
- androidTestCompile 'com.android.support.test:runner:' + rootProject.runnerVersion;
- androidTestCompile 'com.android.support.test:rules:' + rootProject.rulesVersion;
- androidTestCompile 'com.android.support.test.espresso:espresso-core:' + rootProject.espressoVersion;
+ // Android Testing Support Library's runner and rules
+ androidTestImplementation deps.atsl.runner
+ androidTestImplementation deps.atsl.rules
+ androidTestImplementation deps.room.testing
+ androidTestImplementation deps.arch_core.testing
- androidTestCompile ('com.android.support.test.espresso:espresso-contrib:2.2'){
- exclude group: 'com.android.support', module: 'appcompat-v7'
- exclude group: 'com.android.support', module: 'support-v4'
- exclude module: 'recyclerview-v7'
- }
+ // Espresso UI Testing
+ androidTestImplementation deps.espresso.core
+ androidTestImplementation deps.espresso.contrib
+ androidTestImplementation deps.espresso.intents
- // Force usage of dependencies in the test app, since it is internally used by the runner module.
- androidTestCompile 'com.android.support:support-annotations:' + rootProject.supportLibVersion;
- androidTestCompile 'com.android.support:support-v4:' + rootProject.supportLibVersion;
- androidTestCompile 'com.android.support:recyclerview-v7:' + rootProject.supportLibVersion;
+ // Resolve conflicts between main and test APK:
+ androidTestImplementation deps.support.annotations
+ androidTestImplementation deps.support.v4
+ androidTestImplementation deps.support.app_compat
+ androidTestImplementation deps.support.design
}
\ No newline at end of file
diff --git a/BasicSample/app/src/androidTest/java/com/example/android/persistence/EspressoTestUtil.java b/BasicSample/app/src/androidTest/java/com/example/android/persistence/EspressoTestUtil.java
new file mode 100644
index 000000000..2753bd75f
--- /dev/null
+++ b/BasicSample/app/src/androidTest/java/com/example/android/persistence/EspressoTestUtil.java
@@ -0,0 +1,71 @@
+package com.example.android.persistence;
+
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.support.test.rule.ActivityTestRule;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+
+/**
+ * Utility methods for espresso tests.
+ */
+public class EspressoTestUtil {
+ /**
+ * Disables progress bar animations for the views of the given activity rule
+ *
+ * @param activityTestRule The activity rule whose views will be checked
+ */
+ public static void disableAnimations(
+ ActivityTestRule extends FragmentActivity> activityTestRule) {
+ activityTestRule.getActivity().getSupportFragmentManager()
+ .registerFragmentLifecycleCallbacks(
+ new FragmentManager.FragmentLifecycleCallbacks() {
+ @Override
+ public void onFragmentViewCreated(FragmentManager fm, Fragment f, View v,
+ Bundle savedInstanceState) {
+ // traverse all views, if any is a progress bar, replace its animation
+ traverseViews(v);
+ }
+ }, true);
+ }
+
+ private static void traverseViews(View view) {
+ if (view instanceof ViewGroup) {
+ traverseViewGroup((ViewGroup) view);
+ } else {
+ if (view instanceof ProgressBar) {
+ disableProgressBarAnimation((ProgressBar) view);
+ }
+ }
+ }
+
+ private static void traverseViewGroup(ViewGroup view) {
+ if (view instanceof RecyclerView) {
+ disableRecyclerViewAnimations((RecyclerView) view);
+ } else {
+ final int count = view.getChildCount();
+ for (int i = 0; i < count; i++) {
+ traverseViews(view.getChildAt(i));
+ }
+ }
+ }
+
+ private static void disableRecyclerViewAnimations(RecyclerView view) {
+ view.setItemAnimator(null);
+ }
+
+ /**
+ * necessary to run tests on older API levels where progress bar uses handler loop to animate.
+ *
+ * @param progressBar The progress bar whose animation will be swapped with a drawable
+ */
+ private static void disableProgressBarAnimation(ProgressBar progressBar) {
+ progressBar.setIndeterminateDrawable(new ColorDrawable(Color.BLUE));
+ }
+}
\ No newline at end of file
diff --git a/BasicSample/app/src/androidTest/java/com/example/android/persistence/LiveDataTestUtil.java b/BasicSample/app/src/androidTest/java/com/example/android/persistence/LiveDataTestUtil.java
new file mode 100644
index 000000000..88097b3b3
--- /dev/null
+++ b/BasicSample/app/src/androidTest/java/com/example/android/persistence/LiveDataTestUtil.java
@@ -0,0 +1,48 @@
+/*
+ * 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 getValue(final LiveData liveData) throws InterruptedException {
+ final Object[] data = new Object[1];
+ final CountDownLatch latch = new CountDownLatch(1);
+ Observer observer = new Observer() {
+ @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];
+ }
+}
diff --git a/BasicSample/app/src/androidTest/java/com/example/android/persistence/MainActivityTest.java b/BasicSample/app/src/androidTest/java/com/example/android/persistence/MainActivityTest.java
deleted file mode 100644
index b9b899c39..000000000
--- a/BasicSample/app/src/androidTest/java/com/example/android/persistence/MainActivityTest.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * 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.arch.lifecycle.Observer;
-import android.arch.lifecycle.ViewModelProviders;
-import android.support.annotation.Nullable;
-import android.support.test.espresso.Espresso;
-import android.support.test.espresso.IdlingResource;
-import android.support.test.espresso.contrib.RecyclerViewActions;
-import android.support.test.rule.ActivityTestRule;
-import android.support.v4.app.Fragment;
-
-import com.example.android.persistence.db.entity.ProductEntity;
-import com.example.android.persistence.viewmodel.ProductListViewModel;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import static android.support.test.espresso.Espresso.onView;
-import static android.support.test.espresso.action.ViewActions.click;
-import static android.support.test.espresso.assertion.ViewAssertions.matches;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription;
-import static android.support.test.espresso.matcher.ViewMatchers.withText;
-import static org.hamcrest.core.IsNot.not;
-
-public class MainActivityTest {
-
- @Rule
- public ActivityTestRule mActivityRule = new ActivityTestRule<>(
- MainActivity.class);
-
- private SimpleIdlingResource idlingRes = new SimpleIdlingResource();
-
- @Before
- public void idlingResourceSetup() {
-
- Espresso.registerIdlingResources(idlingRes);
- // There's always
- idlingRes.setIdleNow(false);
-
- ProductListViewModel productListViewModel = getProductListViewModel();
-
- // Subscribe to ProductListViewModel's products list observable to figure out when the
- // app is idle.
- productListViewModel.getProducts().observeForever(new Observer>() {
- @Override
- public void onChanged(@Nullable List productEntities) {
- if (productEntities != null) {
- idlingRes.setIdleNow(true);
- }
- }
- });
- }
-
- @Test
- public void clickOnFirstItem_opensComments() {
- // When clicking on the first product
- onView(withContentDescription(R.string.cd_products_list))
- .perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
-
- // Then the second screen with the comments should appear.
- onView(withContentDescription(R.string.cd_comments_list))
- .check(matches(isDisplayed()));
-
- // Then the second screen with the comments should appear.
- onView(withContentDescription(R.string.cd_product_name))
- .check(matches(not(withText(""))));
-
- }
-
- /** Gets the ViewModel for the current fragment */
- private ProductListViewModel getProductListViewModel() {
- MainActivity activity = mActivityRule.getActivity();
-
- Fragment productListFragment = activity.getSupportFragmentManager()
- .findFragmentByTag(ProductListFragment.TAG);
-
- return ViewModelProviders.of(productListFragment)
- .get(ProductListViewModel.class);
- }
-
- private static class SimpleIdlingResource implements IdlingResource {
-
- // written from main thread, read from any thread.
- private volatile ResourceCallback mResourceCallback;
-
- private AtomicBoolean mIsIdleNow = new AtomicBoolean(true);
-
- public void setIdleNow(boolean idleNow) {
- mIsIdleNow.set(idleNow);
- if (idleNow) {
- mResourceCallback.onTransitionToIdle();
- }
- }
-
- @Override
- public String getName() {
- return "Simple idling resource";
- }
-
- @Override
- public boolean isIdleNow() {
- return mIsIdleNow.get();
- }
-
- @Override
- public void registerIdleTransitionCallback(ResourceCallback callback) {
- mResourceCallback = callback;
- }
- }
-}
\ No newline at end of file
diff --git a/BasicSample/app/src/androidTest/java/com/example/android/persistence/db/CommentDaoTest.java b/BasicSample/app/src/androidTest/java/com/example/android/persistence/db/CommentDaoTest.java
new file mode 100644
index 000000000..3cfa72594
--- /dev/null
+++ b/BasicSample/app/src/androidTest/java/com/example/android/persistence/db/CommentDaoTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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 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 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 comments = LiveDataTestUtil.getValue(mCommentDao.loadComments(
+ (COMMENT_ENTITY.getProductId())));
+
+ assertThat(comments.size(), is(1));
+ }
+
+}
diff --git a/BasicSample/app/src/androidTest/java/com/example/android/persistence/db/ProductDaoTest.java b/BasicSample/app/src/androidTest/java/com/example/android/persistence/db/ProductDaoTest.java
new file mode 100644
index 000000000..5d049b884
--- /dev/null
+++ b/BasicSample/app/src/androidTest/java/com/example/android/persistence/db/ProductDaoTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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 products = LiveDataTestUtil.getValue(mProductDao.loadAllProducts());
+
+ assertTrue(products.isEmpty());
+ }
+
+ @Test
+ public void getProductsAfterInserted() throws InterruptedException {
+ mProductDao.insertAll(PRODUCTS);
+
+ List 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()));
+ }
+
+}
diff --git a/BasicSample/app/src/androidTest/java/com/example/android/persistence/db/TestData.java b/BasicSample/app/src/androidTest/java/com/example/android/persistence/db/TestData.java
new file mode 100644
index 000000000..97fd530b7
--- /dev/null
+++ b/BasicSample/app/src/androidTest/java/com/example/android/persistence/db/TestData.java
@@ -0,0 +1,30 @@
+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 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 COMMENTS = Arrays.asList(COMMENT_ENTITY, COMMENT_ENTITY2);
+
+
+}
diff --git a/BasicSample/app/src/androidTest/java/com/example/android/persistence/ui/MainActivityTest.java b/BasicSample/app/src/androidTest/java/com/example/android/persistence/ui/MainActivityTest.java
new file mode 100644
index 000000000..da0a5e13d
--- /dev/null
+++ b/BasicSample/app/src/androidTest/java/com/example/android/persistence/ui/MainActivityTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.ui;
+
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription;
+import static android.support.test.espresso.matcher.ViewMatchers.withText;
+
+import static org.hamcrest.core.IsNot.not;
+
+import android.arch.core.executor.testing.CountingTaskExecutorRule;
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.Observer;
+import android.support.annotation.Nullable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.espresso.contrib.RecyclerViewActions;
+import android.support.test.espresso.matcher.ViewMatchers;
+import android.support.test.rule.ActivityTestRule;
+
+import com.example.android.persistence.AppExecutors;
+import com.example.android.persistence.EspressoTestUtil;
+import com.example.android.persistence.R;
+import com.example.android.persistence.db.AppDatabase;
+
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class MainActivityTest {
+
+ @Rule
+ public ActivityTestRule mActivityRule = new ActivityTestRule<>(
+ MainActivity.class);
+
+ @Rule
+ public CountingTaskExecutorRule mCountingTaskExecutorRule = new CountingTaskExecutorRule();
+
+ public MainActivityTest() {
+ // delete the database
+ InstrumentationRegistry.getTargetContext().deleteDatabase(AppDatabase.DATABASE_NAME);
+ }
+
+ @Before
+ public void disableRecyclerViewAnimations() {
+ // Disable RecyclerView animations
+ EspressoTestUtil.disableAnimations(mActivityRule);
+ }
+
+ @Before
+ public void waitForDbCreation() throws Throwable {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final LiveData databaseCreated = AppDatabase.getInstance(
+ InstrumentationRegistry.getTargetContext(), new AppExecutors())
+ .getDatabaseCreated();
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ databaseCreated.observeForever(new Observer() {
+ @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));
+ }
+
+ @Test
+ public void clickOnFirstItem_opensComments() throws Throwable {
+ drain();
+ // When clicking on the first product
+ onView(ViewMatchers.withContentDescription(R.string.cd_products_list))
+ .perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
+ drain();
+ // Then the second screen with the comments should appear.
+ onView(withContentDescription(R.string.cd_comments_list))
+ .check(matches(isDisplayed()));
+ drain();
+ // Then the second screen with the comments should appear.
+ onView(withContentDescription(R.string.cd_product_name))
+ .check(matches(not(withText(""))));
+
+ }
+
+ private void drain() throws TimeoutException, InterruptedException {
+ mCountingTaskExecutorRule.drainTasks(1, TimeUnit.MINUTES);
+ }
+}
\ No newline at end of file
diff --git a/BasicSample/app/src/main/AndroidManifest.xml b/BasicSample/app/src/main/AndroidManifest.xml
index 322d95787..375666091 100644
--- a/BasicSample/app/src/main/AndroidManifest.xml
+++ b/BasicSample/app/src/main/AndroidManifest.xml
@@ -18,21 +18,22 @@
-
-
+
+
-
+
-
+
-
+
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/AppExecutors.java b/BasicSample/app/src/main/java/com/example/android/persistence/AppExecutors.java
new file mode 100644
index 000000000..a7a94b1d6
--- /dev/null
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/AppExecutors.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ *
+ * 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);
+ }
+ }
+}
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/BasicApp.java b/BasicSample/app/src/main/java/com/example/android/persistence/BasicApp.java
new file mode 100644
index 000000000..7900a892e
--- /dev/null
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/BasicApp.java
@@ -0,0 +1,44 @@
+/*
+ * 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());
+ }
+}
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/DataRepository.java b/BasicSample/app/src/main/java/com/example/android/persistence/DataRepository.java
new file mode 100644
index 000000000..72c9096d0
--- /dev/null
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/DataRepository.java
@@ -0,0 +1,59 @@
+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> 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> getProducts() {
+ return mObservableProducts;
+ }
+
+ public LiveData loadProduct(final int productId) {
+ return mDatabase.productDao().loadProduct(productId);
+ }
+
+ public LiveData> loadComments(final int productId) {
+ return mDatabase.commentDao().loadComments(productId);
+ }
+}
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/db/AppDatabase.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/AppDatabase.java
index ce0afe4b2..dcc120f1d 100644
--- a/BasicSample/app/src/main/java/com/example/android/persistence/db/AppDatabase.java
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/db/AppDatabase.java
@@ -16,23 +16,111 @@
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 android.support.annotation.VisibleForTesting;
+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;
+
+ @VisibleForTesting
+ public static final String DATABASE_NAME = "basic-sample-db";
public abstract ProductDao productDao();
public abstract CommentDao commentDao();
+
+ private final MutableLiveData 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 products = DataGenerator.generateProducts();
+ List 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 products,
+ final List comments) {
+ database.runInTransaction(() -> {
+ database.productDao().insertAll(products);
+ database.commentDao().insertAll(comments);
+ });
+ }
+
+ private static void addDelay() {
+ try {
+ Thread.sleep(4000);
+ } catch (InterruptedException ignored) {
+ }
+ }
+
+ public LiveData getDatabaseCreated() {
+ return mIsDatabaseCreated;
+ }
}
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseInitUtil.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/DataGenerator.java
similarity index 79%
rename from BasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseInitUtil.java
rename to BasicSample/app/src/main/java/com/example/android/persistence/db/DataGenerator.java
index c68fe8fad..6c0b0b0d9 100644
--- a/BasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseInitUtil.java
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/db/DataGenerator.java
@@ -26,8 +26,10 @@
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"};
@@ -37,19 +39,10 @@ class DatabaseInitUtil {
"is finally here", "is recommended by Stan S. Stanman",
"is the best sold product on Mêlée Island", "is \uD83D\uDCAF", "is ❤️", "is fine"};
private static final String[] COMMENTS = new String[]{
- "Comment 1", "Comment 2", "Comment 3", "Comment 4", "Comment 5", "Comment 6",
- };
+ "Comment 1", "Comment 2", "Comment 3", "Comment 4", "Comment 5", "Comment 6"};
- static void initializeDb(AppDatabase db) {
+ public static List generateProducts() {
List products = new ArrayList<>(FIRST.length * SECOND.length);
- List comments = new ArrayList<>();
-
- generateData(products, comments);
-
- insertData(db, products, comments);
- }
-
- private static void generateData(List products, List comments) {
Random rnd = new Random();
for (int i = 0; i < FIRST.length; i++) {
for (int j = 0; j < SECOND.length; j++) {
@@ -61,6 +54,13 @@ private static void generateData(List products, List generateCommentsForProducts(
+ final List products) {
+ List comments = new ArrayList<>();
+ Random rnd = new Random();
for (Product product : products) {
int commentsNumber = rnd.nextInt(5) + 1;
@@ -73,16 +73,7 @@ private static void generateData(List products, List products, List comments) {
- db.beginTransaction();
- try {
- db.productDao().insertAll(products);
- db.commentDao().insertAll(comments);
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
+ return comments;
}
}
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseCreator.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseCreator.java
deleted file mode 100644
index 5ecb8847a..000000000
--- a/BasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseCreator.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * 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.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 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 isDatabaseCreated() {
- return mIsDatabaseCreated;
- }
-
- @Nullable
- public AppDatabase getDatabase() {
- return mDb;
- }
-
- /**
- * Creates or returns a previously-created database.
- *
- * Although this uses an AsyncTask which currently uses a serial executor, it's thread-safe.
- */
- 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() {
-
- @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) {}
- }
-}
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/db/dao/CommentDao.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/dao/CommentDao.java
index b351526cf..062c24586 100644
--- a/BasicSample/app/src/main/java/com/example/android/persistence/db/dao/CommentDao.java
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/db/dao/CommentDao.java
@@ -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;
@@ -36,5 +35,5 @@ public interface CommentDao {
List loadCommentsSync(int productId);
@Insert(onConflict = OnConflictStrategy.REPLACE)
- void insertAll(List products);
+ void insertAll(List comments);
}
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/db/entity/CommentEntity.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/entity/CommentEntity.java
index e3d5702c1..21a56b23b 100644
--- a/BasicSample/app/src/main/java/com/example/android/persistence/db/entity/CommentEntity.java
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/db/entity/CommentEntity.java
@@ -20,17 +20,19 @@
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 void setPostedAt(Date postedAt) {
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;
}
}
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/db/entity/ProductEntity.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/entity/ProductEntity.java
index af1b79a9b..35a45c790 100644
--- a/BasicSample/app/src/main/java/com/example/android/persistence/db/entity/ProductEntity.java
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/db/entity/ProductEntity.java
@@ -68,6 +68,13 @@ public void setPrice(int price) {
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();
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/MainActivity.java b/BasicSample/app/src/main/java/com/example/android/persistence/ui/MainActivity.java
similarity index 89%
rename from BasicSample/app/src/main/java/com/example/android/persistence/MainActivity.java
rename to BasicSample/app/src/main/java/com/example/android/persistence/ui/MainActivity.java
index aee825590..c53b587a8 100644
--- a/BasicSample/app/src/main/java/com/example/android/persistence/MainActivity.java
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/ui/MainActivity.java
@@ -14,15 +14,16 @@
* 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.support.v7.app.AppCompatActivity;
-import android.arch.lifecycle.LifecycleActivity;
+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);
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java b/BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java
index e54c1ca2c..3f1736304 100644
--- a/BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java
@@ -65,12 +65,12 @@ public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
- Product product = productList.get(newItemPosition);
- Product old = productList.get(oldItemPosition);
- return product.getId() == old.getId()
- && Objects.equals(product.getDescription(), old.getDescription())
- && Objects.equals(product.getName(), old.getName())
- && product.getPrice() == old.getPrice();
+ Product newProduct = productList.get(newItemPosition);
+ Product oldProduct = mProductList.get(oldItemPosition);
+ return newProduct.getId() == oldProduct.getId()
+ && Objects.equals(newProduct.getDescription(), oldProduct.getDescription())
+ && Objects.equals(newProduct.getName(), oldProduct.getName())
+ && newProduct.getPrice() == oldProduct.getPrice();
}
});
mProductList = productList;
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/ProductFragment.java b/BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductFragment.java
similarity index 93%
rename from BasicSample/app/src/main/java/com/example/android/persistence/ProductFragment.java
rename to BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductFragment.java
index 4c4014f33..fc38528f2 100644
--- a/BasicSample/app/src/main/java/com/example/android/persistence/ProductFragment.java
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductFragment.java
@@ -14,29 +14,28 @@
* 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;
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 +67,7 @@ public void onClick(Comment comment) {
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
+
ProductViewModel.Factory factory = new ProductViewModel.Factory(
getActivity().getApplication(), getArguments().getInt(KEY_PRODUCT_ID));
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/ProductListFragment.java b/BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductListFragment.java
similarity index 89%
rename from BasicSample/app/src/main/java/com/example/android/persistence/ProductListFragment.java
rename to BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductListFragment.java
index 772bc9cba..45e441e9e 100644
--- a/BasicSample/app/src/main/java/com/example/android/persistence/ProductListFragment.java
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductListFragment.java
@@ -14,29 +14,28 @@
* 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;
import android.arch.lifecycle.Observer;
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";
@@ -76,6 +75,9 @@ public void onChanged(@Nullable List myProducts) {
} else {
mBinding.setIsLoading(true);
}
+ // espresso does not know how to wait for data binding's loop so we execute changes
+ // sync.
+ mBinding.executePendingBindings();
}
});
}
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java b/BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java
index 8905b14f9..00832b38c 100644
--- a/BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java
@@ -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> mObservableProducts;
+ // MediatorLiveData can observe other LiveData objects and react on their emissions.
+ private final MediatorLiveData> 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 databaseCreated = databaseCreator.isDatabaseCreated();
- mObservableProducts = Transformations.switchMap(databaseCreated,
- new Function>>() {
- @Override
- public LiveData> 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> 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);
}
/**
diff --git a/BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java b/BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java
index 08534c3af..2867e940b 100644
--- a/BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java
+++ b/BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java
@@ -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 @@
public class ProductViewModel extends AndroidViewModel {
- private static final MutableLiveData ABSENT = new MutableLiveData();
- {
- //noinspection unchecked
- ABSENT.setValue(null);
- }
-
private final LiveData mObservableProduct;
public ObservableField product = new ObservableField<>();
@@ -49,42 +41,15 @@ public class ProductViewModel extends AndroidViewModel {
private final LiveData> 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>>() {
- @Override
- public LiveData> 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>() {
- @Override
- public LiveData 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 static class Factory extends ViewModelProvider.NewInstanceFactory {
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 create(Class modelClass) {
//noinspection unchecked
- return (T) new ProductViewModel(mApplication, mProductId);
+ return (T) new ProductViewModel(mApplication, mRepository, mProductId);
}
}
}
diff --git a/BasicSample/build.gradle b/BasicSample/build.gradle
index c1d295778..63727c0ad 100644
--- a/BasicSample/build.gradle
+++ b/BasicSample/build.gradle
@@ -17,11 +17,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- repositories {
- jcenter()
- }
+ apply from: 'versions.gradle'
+ addRepos(repositories)
dependencies {
- classpath 'com.android.tools.build:gradle:2.3.1'
+ classpath deps.android_gradle_plugin
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -29,24 +28,9 @@ buildscript {
}
allprojects {
- repositories {
- jcenter()
- maven {
- url "https://maven.google.com/"
- }
- }
+ addRepos(repositories)
}
task clean(type: Delete) {
delete rootProject.buildDir
}
-
-ext {
- buildToolsVersion = "25.0.2"
- supportLibVersion = "25.3.1"
- runnerVersion = "0.5"
- rulesVersion = "0.5"
- espressoVersion = "2.2.2"
- archLifecycleVersion = "1.0.0-alpha1"
- archRoomVersion = "1.0.0-alpha1"
-}
\ No newline at end of file
diff --git a/BasicSample/gradle/wrapper/gradle-wrapper.properties b/BasicSample/gradle/wrapper/gradle-wrapper.properties
index 37b4e8c5f..612a960ca 100644
--- a/BasicSample/gradle/wrapper/gradle-wrapper.properties
+++ b/BasicSample/gradle/wrapper/gradle-wrapper.properties
@@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/BasicSample/versions.gradle b/BasicSample/versions.gradle
new file mode 100644
index 000000000..85822d50b
--- /dev/null
+++ b/BasicSample/versions.gradle
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+
+
+/**
+* Shared file between builds so that they can all use the same dependencies and
+* maven repositories.
+**/
+ext.deps = [:]
+def versions = [:]
+versions.arch_core = "1.1.1"
+versions.room = "1.1.0-beta2"
+versions.lifecycle = "1.1.1"
+versions.support = "27.1.0"
+versions.dagger = "2.11"
+versions.junit = "4.12"
+versions.espresso = "3.0.1"
+versions.retrofit = "2.3.0"
+versions.okhttp_logging_interceptor = "3.9.0"
+versions.mockwebserver = "3.8.1"
+versions.apache_commons = "2.5"
+versions.mockito = "2.7.19"
+versions.mockito_all = "1.10.19"
+versions.dexmaker = "2.2.0"
+versions.constraint_layout = "1.0.2"
+versions.glide = "3.8.0"
+versions.timber = "4.5.1"
+versions.android_gradle_plugin = "3.0.1"
+versions.rxjava2 = "2.1.3"
+versions.rx_android = "2.0.1"
+versions.atsl_runner = "1.0.1"
+versions.atsl_rules = "1.0.1"
+versions.hamcrest = "1.3"
+versions.kotlin = "1.2.20"
+versions.paging = "1.0.0-beta1"
+def deps = [:]
+
+def support = [:]
+support.annotations = "com.android.support:support-annotations:$versions.support"
+support.app_compat = "com.android.support:appcompat-v7:$versions.support"
+support.recyclerview = "com.android.support:recyclerview-v7:$versions.support"
+support.cardview = "com.android.support:cardview-v7:$versions.support"
+support.design = "com.android.support:design:$versions.support"
+support.v4 = "com.android.support:support-v4:$versions.support"
+support.core_utils = "com.android.support:support-core-utils:$versions.support"
+deps.support = support
+
+def room = [:]
+room.runtime = "android.arch.persistence.room:runtime:$versions.room"
+room.compiler = "android.arch.persistence.room:compiler:$versions.room"
+room.rxjava2 = "android.arch.persistence.room:rxjava2:$versions.room"
+room.testing = "android.arch.persistence.room:testing:$versions.room"
+deps.room = room
+
+def lifecycle = [:]
+lifecycle.runtime = "android.arch.lifecycle:runtime:$versions.lifecycle"
+lifecycle.extensions = "android.arch.lifecycle:extensions:$versions.lifecycle"
+lifecycle.java8 = "android.arch.lifecycle:common-java8:$versions.lifecycle"
+lifecycle.compiler = "android.arch.lifecycle:compiler:$versions.lifecycle"
+deps.lifecycle = lifecycle
+
+def arch_core = [:]
+arch_core.testing = "android.arch.core:core-testing:$versions.arch_core"
+deps.arch_core = arch_core
+
+def retrofit = [:]
+retrofit.runtime = "com.squareup.retrofit2:retrofit:$versions.retrofit"
+retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit"
+retrofit.mock = "com.squareup.retrofit2:retrofit-mock:$versions.retrofit"
+deps.retrofit = retrofit
+deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:${versions.okhttp_logging_interceptor}"
+
+def dagger = [:]
+dagger.runtime = "com.google.dagger:dagger:$versions.dagger"
+dagger.android = "com.google.dagger:dagger-android:$versions.dagger"
+dagger.android_support = "com.google.dagger:dagger-android-support:$versions.dagger"
+dagger.compiler = "com.google.dagger:dagger-compiler:$versions.dagger"
+dagger.android_support_compiler = "com.google.dagger:dagger-android-processor:$versions.dagger"
+
+deps.dagger = dagger
+
+def espresso = [:]
+espresso.core = "com.android.support.test.espresso:espresso-core:$versions.espresso"
+espresso.contrib = "com.android.support.test.espresso:espresso-contrib:$versions.espresso"
+espresso.intents = "com.android.support.test.espresso:espresso-intents:$versions.espresso"
+deps.espresso = espresso
+
+def atsl = [:]
+atsl.runner = "com.android.support.test:runner:$versions.atsl_runner"
+atsl.rules = "com.android.support.test:rules:$versions.atsl_runner"
+deps.atsl = atsl
+
+def mockito = [:]
+mockito.core = "org.mockito:mockito-core:$versions.mockito"
+mockito.all = "org.mockito:mockito-all:$versions.mockito_all"
+deps.mockito = mockito
+
+def kotlin = [:]
+kotlin.stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jre7:$versions.kotlin"
+kotlin.test = "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin"
+kotlin.plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
+
+deps.kotlin = kotlin
+deps.paging = "android.arch.paging:runtime:$versions.paging"
+deps.glide = "com.github.bumptech.glide:glide:$versions.glide"
+deps.dexmaker = "com.linkedin.dexmaker:dexmaker-mockito:$versions.dexmaker"
+deps.constraint_layout = "com.android.support.constraint:constraint-layout:$versions.constraint_layout"
+deps.timber = "com.jakewharton.timber:timber:$versions.timber"
+deps.junit = "junit:junit:$versions.junit"
+deps.mock_web_server = "com.squareup.okhttp3:mockwebserver:$versions.mockwebserver"
+deps.rxjava2 = "io.reactivex.rxjava2:rxjava:$versions.rxjava2"
+deps.rx_android = "io.reactivex.rxjava2:rxandroid:$versions.rx_android"
+deps.hamcrest = "org.hamcrest:hamcrest-all:$versions.hamcrest"
+deps.android_gradle_plugin = "com.android.tools.build:gradle:$versions.android_gradle_plugin"
+ext.deps = deps
+
+def build_versions = [:]
+build_versions.min_sdk = 14
+build_versions.target_sdk = 26
+build_versions.build_tools = "27.0.3"
+ext.build_versions = build_versions
+
+
+def addRepos(RepositoryHandler handler) {
+ handler.google()
+ handler.jcenter()
+ handler.maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
+}
+ext.addRepos = this.&addRepos
diff --git a/GithubBrowserSample/.google/packaging.yaml b/GithubBrowserSample/.google/packaging.yaml
new file mode 100644
index 000000000..108903dd1
--- /dev/null
+++ b/GithubBrowserSample/.google/packaging.yaml
@@ -0,0 +1,41 @@
+#
+# 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.
+
+# GOOGLE SAMPLE PACKAGING DATA
+#
+# This file is used by Google as part of our samples packaging process.
+# End users may safely ignore this file. It has no relevance to other systems.
+---
+# Values: {DRAFT | PUBLISHED | INTERNAL | DEPRECATED | SUPERCEDED}
+status: PUBLISHED
+
+# Optional, put additional explanation here for DEPRECATED or SUPERCEDED.
+# statusNote:
+
+# See http://go/sample-categories
+technologies: [Android]
+categories: [Architecture]
+languages: [Java]
+solutions: [Mobile]
+
+# May be omitted if unpublished
+github: googlesamples/android-architecture-components
+
+# Values: BEGINNER | INTERMEDIATE | ADVANCED | EXPERT
+level: ADVANCED
+
+# Default: apache2. May be omitted for most samples.
+# Alternatives: apache2-android (for AOSP)
+license: apache2
diff --git a/GithubBrowserSample/README.md b/GithubBrowserSample/README.md
index 7d06247bc..40815a66c 100644
--- a/GithubBrowserSample/README.md
+++ b/GithubBrowserSample/README.md
@@ -1,4 +1,5 @@
Github Browser Sample with Android Architecture Components
+===========================================================
This is a sample app that uses Android Architecture Components with Dagger 2.
@@ -6,9 +7,12 @@ This is a sample app that uses Android Architecture Components with Dagger 2.
with [Architecture Components][arch], you are highly recommended to check other examples
in this repository first.
-## Functionality
+Introduction
+-------------
+
+### Functionality
The app is composed of 3 main screens.
-### SearchFragment
+#### SearchFragment
Allows you to search repositories on Github.
Each search result is kept in the database in `RepoSearchResult` table where
the list of repository IDs are denormalized into a single column.
@@ -21,41 +25,41 @@ Database is updated with the new list of repository ids.
perform well on lower end devices. Instead of manually writing lazy
adapters, we've decided to wait until the built in support in Room is released.
-### RepoFragment
+#### RepoFragment
This fragment displays the details of a repository and its contributors.
-### UserFragment
+#### UserFragment
This fragment displays a user and their repositories.
-## Building
+### Building
You can open the project in Android studio and press run.
-## Testing
+### Testing
The project uses both instrumentation tests that run on the device
and local unit tests that run on your computer.
To run both of them and generate a coverage report, you can run:
`./gradlew fullCoverageReport` (requires a connected device or an emulator)
-### Device Tests
-#### UI Tests
+#### Device Tests
+##### UI Tests
The projects uses Espresso for UI testing. Since each fragment
is limited to a ViewModel, each test mocks related ViewModel to
run the tests.
-#### Database Tests
+##### Database Tests
The project creates an in memory database for each database test but still
runs them on the device.
-### Local Unit Tests
-#### ViewModel Tests
+#### Local Unit Tests
+##### ViewModel Tests
Each ViewModel is tested using local unit tests with mock Repository
implementations.
-#### Repository Tests
+##### Repository Tests
Each Repository is tested using local unit tests with mock web service and
mock database.
-#### Webservice Tests
+##### Webservice Tests
The project uses [MockWebServer][mockwebserver] project to test REST api interactions.
-## Libraries
+### Libraries
* [Android Support Library][support-lib]
* [Android Architecture Components][arch]
* [Android Data Binding][data-binding]
@@ -77,3 +81,23 @@ The project uses [MockWebServer][mockwebserver] project to test REST api interac
[glide]: https://github.com/bumptech/glide
[timber]: https://github.com/JakeWharton/timber
[mockito]: http://site.mockito.org
+
+License
+--------
+
+Copyright 2017 The Android Open Source Project, Inc.
+
+Licensed to the Apache Software Foundation (ASF) under one or more contributor
+license agreements. See the NOTICE file distributed with this work for
+additional information regarding copyright ownership. The ASF licenses this
+file to you 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.
diff --git a/GithubBrowserSample/app/build.gradle b/GithubBrowserSample/app/build.gradle
index 0d833118a..48bc895b8 100644
--- a/GithubBrowserSample/app/build.gradle
+++ b/GithubBrowserSample/app/build.gradle
@@ -17,12 +17,12 @@
apply plugin: 'com.android.application'
apply plugin: 'jacoco'
android {
- compileSdkVersion 25
- buildToolsVersion "25.0.2"
+ compileSdkVersion build_versions.target_sdk
+ buildToolsVersion build_versions.build_tools
defaultConfig {
applicationId "com.android.example.github"
- minSdkVersion 14
- targetSdkVersion 25
+ minSdkVersion build_versions.min_sdk
+ targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "com.android.example.github.util.GithubTestRunner"
@@ -48,7 +48,7 @@ android {
test.java.srcDirs += "src/test-common/java"
}
lintOptions {
- disable 'GoogleAppIndexingWarning'
+ lintConfig rootProject.file('lint.xml')
}
}
@@ -57,54 +57,57 @@ jacoco {
}
dependencies {
- compile "com.android.support:appcompat-v7:$support_version"
- compile "com.android.support:recyclerview-v7:$support_version"
- compile "com.android.support:cardview-v7:$support_version"
- compile "com.android.support:design:$support_version"
- compile "android.arch.persistence.room:runtime:$arch_version"
- compile "android.arch.lifecycle:runtime:$arch_version"
- compile "android.arch.lifecycle:extensions:$arch_version"
- compile "com.squareup.retrofit2:retrofit:$retrofit_version"
- compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
- compile "com.github.bumptech.glide:glide:$glide_version"
+ implementation deps.support.app_compat
+ implementation deps.support.recyclerview
+ implementation deps.support.cardview
+ implementation deps.support.design
+ implementation deps.room.runtime
+ implementation deps.lifecycle.runtime
+ implementation deps.lifecycle.extensions
+ implementation deps.lifecycle.java8
+ implementation deps.retrofit.runtime
+ implementation deps.retrofit.gson
+ implementation deps.glide
- compile "com.google.dagger:dagger:$dagger_version"
- compile "com.google.dagger:dagger-android:$dagger_version"
- compile "com.google.dagger:dagger-android-support:$dagger_version"
- compile "com.android.support.constraint:constraint-layout:$constraint_layout_version"
+ implementation deps.dagger.runtime
+ implementation deps.dagger.android
+ implementation deps.dagger.android_support
+ implementation deps.constraint_layout
- compile "com.jakewharton.timber:timber:$timber_version"
+ implementation deps.timber
- annotationProcessor "com.google.dagger:dagger-android-processor:$dagger_version"
- annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
- annotationProcessor "android.arch.persistence.room:compiler:$arch_version"
- annotationProcessor "android.arch.lifecycle:compiler:$arch_version"
+ annotationProcessor deps.dagger.android_support_compiler
+ annotationProcessor deps.dagger.compiler
+ annotationProcessor deps.room.compiler
+ annotationProcessor deps.lifecycle.compiler
- testCompile "junit:junit:$junit_version"
- testCompile "com.squareup.okhttp3:mockwebserver:$mockwebserver_version"
- testCompile ("android.arch.core:core-testing:$arch_version", {
+ testImplementation deps.junit
+ testImplementation deps.mock_web_server
+ testImplementation (deps.arch_core.testing, {
exclude group: 'com.android.support', module: 'support-compat'
exclude group: 'com.android.support', module: 'support-annotations'
exclude group: 'com.android.support', module: 'support-core-utils'
})
+ testImplementation deps.mockito.core
- androidTestCompile "com.android.support:appcompat-v7:$support_version"
- androidTestCompile "com.android.support:recyclerview-v7:$support_version"
- androidTestCompile "com.android.support:support-v4:$support_version"
- androidTestCompile "com.android.support:design:$support_version"
+ androidTestImplementation deps.support.app_compat
+ androidTestImplementation deps.support.recyclerview
+ androidTestImplementation deps.support.cardview
+ androidTestImplementation deps.support.design
- androidTestCompile("com.android.support.test.espresso:espresso-core:$espresso_version", {
+ androidTestImplementation(deps.espresso.core, {
exclude group: 'com.android.support', module: 'support-annotations'
exclude group: 'com.google.code.findbugs', module: 'jsr305'
})
- androidTestCompile("com.android.support.test.espresso:espresso-contrib:$espresso_version", {
+ androidTestImplementation(deps.espresso.contrib, {
exclude group: 'com.android.support', module: 'support-annotations'
exclude group: 'com.google.code.findbugs', module: 'jsr305'
})
- androidTestCompile("android.arch.core:core-testing:$arch_version", {
+ androidTestImplementation(deps.arch_core.testing, {
})
- androidTestCompile "org.mockito:mockito-android:$mockito_version"
+ androidTestImplementation deps.dexmaker
+ androidTestImplementation deps.mockito.core, { exclude group: 'net.bytebuddy' }
}
task fullCoverageReport(type: JacocoReport) {
diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/repo/RepoFragmentTest.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/repo/RepoFragmentTest.java
index b739934e6..095e1dc30 100644
--- a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/repo/RepoFragmentTest.java
+++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/repo/RepoFragmentTest.java
@@ -16,10 +16,34 @@
package com.android.example.github.ui.repo;
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.test.espresso.matcher.ViewMatchers.withText;
+
+import static org.hamcrest.CoreMatchers.not;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.arch.lifecycle.MutableLiveData;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
import com.android.example.github.R;
import com.android.example.github.binding.FragmentBindingAdapters;
import com.android.example.github.testing.SingleFragmentActivity;
import com.android.example.github.ui.common.NavigationController;
+import com.android.example.github.util.EspressoTestUtil;
import com.android.example.github.util.RecyclerViewMatcher;
import com.android.example.github.util.TaskExecutorWithIdlingResourceRule;
import com.android.example.github.util.TestUtil;
@@ -33,29 +57,9 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import android.arch.lifecycle.MutableLiveData;
-import android.support.annotation.NonNull;
-import android.support.annotation.StringRes;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
-
import java.util.ArrayList;
import java.util.List;
-import static android.support.test.espresso.Espresso.onView;
-import static android.support.test.espresso.action.ViewActions.click;
-import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
-import static android.support.test.espresso.assertion.ViewAssertions.matches;
-import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static android.support.test.espresso.matcher.ViewMatchers.withText;
-import static org.hamcrest.CoreMatchers.not;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(AndroidJUnit4.class)
public class RepoFragmentTest {
@Rule
@@ -75,18 +79,18 @@ public class RepoFragmentTest {
@Before
public void init() {
+ EspressoTestUtil.disableProgressBarAnimations(activityRule);
repoFragment = RepoFragment.create("a", "b");
viewModel = mock(RepoViewModel.class);
fragmentBindingAdapters = mock(FragmentBindingAdapters.class);
navigationController = mock(NavigationController.class);
-
+ doNothing().when(viewModel).setId(anyString(), anyString());
when(viewModel.getRepo()).thenReturn(repo);
when(viewModel.getContributors()).thenReturn(contributors);
repoFragment.viewModelFactory = ViewModelUtil.createFor(viewModel);
repoFragment.dataBindingComponent = () -> fragmentBindingAdapters;
repoFragment.navigationController = navigationController;
-
activityRule.getActivity().setFragment(repoFragment);
}
diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/search/SearchFragmentTest.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/search/SearchFragmentTest.java
index a5435cf3e..8123f28d5 100644
--- a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/search/SearchFragmentTest.java
+++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/search/SearchFragmentTest.java
@@ -16,10 +16,37 @@
package com.android.example.github.ui.search;
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.support.test.espresso.action.ViewActions.pressKey;
+import static android.support.test.espresso.action.ViewActions.typeText;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.test.espresso.matcher.ViewMatchers.withText;
+
+import static org.hamcrest.CoreMatchers.not;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.arch.lifecycle.MutableLiveData;
+import android.support.annotation.NonNull;
+import android.support.test.espresso.contrib.RecyclerViewActions;
+import android.support.test.espresso.matcher.ViewMatchers;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.KeyEvent;
+
import com.android.example.github.R;
import com.android.example.github.binding.FragmentBindingAdapters;
import com.android.example.github.testing.SingleFragmentActivity;
import com.android.example.github.ui.common.NavigationController;
+import com.android.example.github.util.EspressoTestUtil;
import com.android.example.github.util.RecyclerViewMatcher;
import com.android.example.github.util.TaskExecutorWithIdlingResourceRule;
import com.android.example.github.util.TestUtil;
@@ -32,33 +59,9 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import android.arch.lifecycle.MutableLiveData;
-import android.support.annotation.NonNull;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.espresso.contrib.RecyclerViewActions;
-import android.support.test.espresso.matcher.ViewMatchers;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
-import android.view.KeyEvent;
-
import java.util.Arrays;
import java.util.List;
-import static android.support.test.espresso.Espresso.onView;
-import static android.support.test.espresso.action.ViewActions.click;
-import static android.support.test.espresso.action.ViewActions.pressKey;
-import static android.support.test.espresso.action.ViewActions.typeText;
-import static android.support.test.espresso.assertion.ViewAssertions.matches;
-import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static android.support.test.espresso.matcher.ViewMatchers.withText;
-import static org.hamcrest.CoreMatchers.not;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(AndroidJUnit4.class)
public class SearchFragmentTest {
@Rule
@@ -78,9 +81,10 @@ public class SearchFragmentTest {
@Before
public void init() {
+ EspressoTestUtil.disableProgressBarAnimations(activityRule);
SearchFragment searchFragment = new SearchFragment();
viewModel = mock(SearchViewModel.class);
- when(viewModel.getLoadMoreStatus()).thenReturn(loadMoreStatus);
+ doReturn(loadMoreStatus).when(viewModel).getLoadMoreStatus();
when(viewModel.getResults()).thenReturn(results);
fragmentBindingAdapters = mock(FragmentBindingAdapters.class);
@@ -134,6 +138,7 @@ public void loadMore() throws Throwable {
@Test
public void navigateToRepo() throws Throwable {
+ doNothing().when(viewModel).loadNextPage();
Repo repo = TestUtil.createRepo("foo", "bar", "desc");
results.postValue(Resource.success(Arrays.asList(repo)));
onView(withText("desc")).perform(click());
diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/user/UserFragmentTest.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/user/UserFragmentTest.java
index 70bf2bf48..3fd7126a2 100644
--- a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/user/UserFragmentTest.java
+++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/user/UserFragmentTest.java
@@ -16,10 +16,32 @@
package com.android.example.github.ui.user;
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.test.espresso.matcher.ViewMatchers.withText;
+
+import static org.hamcrest.CoreMatchers.not;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.arch.lifecycle.MutableLiveData;
+import android.support.annotation.NonNull;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
import com.android.example.github.R;
import com.android.example.github.binding.FragmentBindingAdapters;
import com.android.example.github.testing.SingleFragmentActivity;
import com.android.example.github.ui.common.NavigationController;
+import com.android.example.github.util.EspressoTestUtil;
import com.android.example.github.util.RecyclerViewMatcher;
import com.android.example.github.util.TestUtil;
import com.android.example.github.util.ViewModelUtil;
@@ -32,27 +54,9 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import android.arch.lifecycle.MutableLiveData;
-import android.support.annotation.NonNull;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
-
import java.util.ArrayList;
import java.util.List;
-import static android.support.test.espresso.Espresso.onView;
-import static android.support.test.espresso.action.ViewActions.click;
-import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
-import static android.support.test.espresso.assertion.ViewAssertions.matches;
-import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static android.support.test.espresso.matcher.ViewMatchers.withText;
-import static org.hamcrest.CoreMatchers.not;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(AndroidJUnit4.class)
public class UserFragmentTest {
@Rule
@@ -66,11 +70,13 @@ public class UserFragmentTest {
private MutableLiveData>> repoListData = new MutableLiveData<>();
@Before
- public void init() {
+ public void init() throws Throwable {
+ EspressoTestUtil.disableProgressBarAnimations(activityRule);
UserFragment fragment = UserFragment.create("foo");
viewModel = mock(UserViewModel.class);
when(viewModel.getUser()).thenReturn(userData);
when(viewModel.getRepositories()).thenReturn(repoListData);
+ doNothing().when(viewModel).setLogin(anyString());
navigationController = mock(NavigationController.class);
fragmentBindingAdapters = mock(FragmentBindingAdapters.class);
@@ -79,6 +85,7 @@ public void init() {
fragment.dataBindingComponent = () -> fragmentBindingAdapters;
activityRule.getActivity().setFragment(fragment);
+ activityRule.runOnUiThread(() -> fragment.binding.get().repoList.setItemAnimator(null));
}
@Test
@@ -89,7 +96,8 @@ public void loading() {
}
@Test
- public void error() {
+ public void error() throws InterruptedException {
+ doNothing().when(viewModel).retry();
userData.postValue(Resource.error("wtf", null));
onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed())));
onView(withId(R.id.error_msg)).check(matches(withText("wtf")));
diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/AutoClearedValueTest.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/AutoClearedValueTest.java
new file mode 100644
index 000000000..1de97e8ec
--- /dev/null
+++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/AutoClearedValueTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.android.example.github.util;
+
+import com.android.example.github.testing.SingleFragmentActivity;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.Fragment;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+
+@RunWith(AndroidJUnit4.class)
+public class AutoClearedValueTest {
+
+ @Rule
+ public ActivityTestRule activityRule =
+ new ActivityTestRule<>(SingleFragmentActivity.class, true, true);
+
+ private TestFragment testFragment;
+
+ @Before
+ public void init() {
+ testFragment = new TestFragment();
+ activityRule.getActivity().setFragment(testFragment);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ @Test
+ public void clearOnReplace() throws Throwable {
+ testFragment.testValue = new AutoClearedValue<>(testFragment, "foo");
+ activityRule.getActivity().replaceFragment(new TestFragment());
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ assertThat(testFragment.testValue.get(), nullValue());
+ }
+
+ @Test
+ public void dontClearForChildFragment() throws Throwable {
+ testFragment.testValue = new AutoClearedValue<>(testFragment, "foo");
+ testFragment.getChildFragmentManager().beginTransaction()
+ .add(new Fragment(), "foo").commit();
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ assertThat(testFragment.testValue.get(), is("foo"));
+ }
+
+ @Test
+ public void dontClearForDialog() throws Throwable {
+ testFragment.testValue = new AutoClearedValue<>(testFragment, "foo");
+ DialogFragment dialogFragment = new DialogFragment();
+ dialogFragment.show(testFragment.getFragmentManager(), "dialog");
+ dialogFragment.dismiss();
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ assertThat(testFragment.testValue.get(), is("foo"));
+ }
+
+ public static class TestFragment extends Fragment {
+
+ AutoClearedValue testValue;
+ }
+}
\ No newline at end of file
diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/EspressoTestUtil.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/EspressoTestUtil.java
new file mode 100644
index 000000000..b9f775d8e
--- /dev/null
+++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/EspressoTestUtil.java
@@ -0,0 +1,77 @@
+/*
+ * 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.android.example.github.util;
+
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.support.test.rule.ActivityTestRule;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+
+/**
+ * Utility methods for espresso tests.
+ */
+public class EspressoTestUtil {
+ /**
+ * Disables progress bar animations for the views of the given activity rule
+ *
+ * @param activityTestRule The activity rule whose views will be checked
+ */
+ public static void disableProgressBarAnimations(
+ ActivityTestRule extends FragmentActivity> activityTestRule) {
+ activityTestRule.getActivity().getSupportFragmentManager()
+ .registerFragmentLifecycleCallbacks(
+ new FragmentManager.FragmentLifecycleCallbacks() {
+ @Override
+ public void onFragmentViewCreated(FragmentManager fm, Fragment f, View v,
+ Bundle savedInstanceState) {
+ // traverse all views, if any is a progress bar, replace its animation
+ traverseViews(v);
+ }
+ }, true);
+ }
+
+ private static void traverseViews(View view) {
+ if (view instanceof ViewGroup) {
+ traverseViewGroup((ViewGroup) view);
+ } else {
+ if (view instanceof ProgressBar) {
+ disableProgressBarAnimation((ProgressBar) view);
+ }
+ }
+ }
+
+ private static void traverseViewGroup(ViewGroup view) {
+ final int count = view.getChildCount();
+ for (int i = 0; i < count; i++) {
+ traverseViews(view.getChildAt(i));
+ }
+ }
+
+ /**
+ * necessary to run tests on older API levels where progress bar uses handler loop to animate.
+ *
+ * @param progressBar The progress bar whose animation will be swapped with a drawable
+ */
+ private static void disableProgressBarAnimation(ProgressBar progressBar) {
+ progressBar.setIndeterminateDrawable(new ColorDrawable(Color.BLUE));
+ }
+}
diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/RecyclerViewMatcher.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/RecyclerViewMatcher.java
index 866028970..6e8321e0b 100644
--- a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/RecyclerViewMatcher.java
+++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/RecyclerViewMatcher.java
@@ -64,8 +64,7 @@ public boolean matchesSafely(View view) {
this.resources = view.getResources();
if (childView == null) {
- RecyclerView recyclerView =
- (RecyclerView) view.getRootView().findViewById(recyclerViewId);
+ RecyclerView recyclerView = view.getRootView().findViewById(recyclerViewId);
if (recyclerView != null && recyclerView.getId() == recyclerViewId) {
RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(position);
if (viewHolder != null) {
diff --git a/GithubBrowserSample/app/src/debug/java/com/android/example/github/testing/SingleFragmentActivity.java b/GithubBrowserSample/app/src/debug/java/com/android/example/github/testing/SingleFragmentActivity.java
index a83a73658..c44640b6e 100644
--- a/GithubBrowserSample/app/src/debug/java/com/android/example/github/testing/SingleFragmentActivity.java
+++ b/GithubBrowserSample/app/src/debug/java/com/android/example/github/testing/SingleFragmentActivity.java
@@ -44,4 +44,9 @@ public void setFragment(Fragment fragment) {
.add(R.id.container, fragment, "TEST")
.commit();
}
+
+ public void replaceFragment(Fragment fragment) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.container, fragment).commit();
+ }
}
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/MainActivity.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/MainActivity.java
index c82bd2ecf..0e47c4197 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/MainActivity.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/MainActivity.java
@@ -16,33 +16,24 @@
package com.android.example.github;
-import com.android.example.github.ui.common.NavigationController;
-
-import android.arch.lifecycle.LifecycleRegistry;
-import android.arch.lifecycle.LifecycleRegistryOwner;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
+import com.android.example.github.ui.common.NavigationController;
+
import javax.inject.Inject;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.support.HasSupportFragmentInjector;
-public class MainActivity extends AppCompatActivity implements LifecycleRegistryOwner,
- HasSupportFragmentInjector {
- private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
+public class MainActivity extends AppCompatActivity implements HasSupportFragmentInjector {
@Inject
DispatchingAndroidInjector dispatchingAndroidInjector;
@Inject
NavigationController navigationController;
- @Override
- public LifecycleRegistry getLifecycle() {
- return lifecycleRegistry;
- }
-
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/api/ApiResponse.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/api/ApiResponse.java
index 33e352540..5586ae3ee 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/api/ApiResponse.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/api/ApiResponse.java
@@ -36,7 +36,7 @@
public class ApiResponse {
private static final Pattern LINK_PATTERN = Pattern
.compile("<([^>]*)>[\\s]*;[\\s]*rel=\"([a-zA-Z0-9]+)\"");
- private static final Pattern PAGE_PATTERN = Pattern.compile("page=(\\d)+");
+ private static final Pattern PAGE_PATTERN = Pattern.compile("\\bpage=(\\d+)");
private static final String NEXT_LINK = "next";
public final int code;
@Nullable
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/db/RepoDao.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/db/RepoDao.java
index ac4572bc1..9a613179d 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/db/RepoDao.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/db/RepoDao.java
@@ -54,7 +54,7 @@ public abstract class RepoDao {
public abstract LiveData load(String login, String name);
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
- @Query("SELECT login, avatarUrl, contributions FROM contributor "
+ @Query("SELECT login, avatarUrl, repoName, repoOwner, contributions FROM contributor "
+ "WHERE repoName = :name AND repoOwner = :owner "
+ "ORDER BY contributions DESC")
public abstract LiveData> loadContributors(String owner, String name);
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppModule.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppModule.java
index 465b746ff..3764b02e6 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppModule.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppModule.java
@@ -16,16 +16,14 @@
package com.android.example.github.di;
+import android.app.Application;
+import android.arch.persistence.room.Room;
+
import com.android.example.github.api.GithubService;
import com.android.example.github.db.GithubDb;
import com.android.example.github.db.RepoDao;
import com.android.example.github.db.UserDao;
import com.android.example.github.util.LiveDataCallAdapterFactory;
-import com.android.example.github.viewmodel.GithubViewModelFactory;
-
-import android.app.Application;
-import android.arch.lifecycle.ViewModelProvider;
-import android.arch.persistence.room.Room;
import javax.inject.Singleton;
@@ -34,7 +32,7 @@
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
-@Module(subcomponents = ViewModelSubComponent.class)
+@Module(includes = ViewModelModule.class)
class AppModule {
@Singleton @Provides
GithubService provideGithubService() {
@@ -60,11 +58,4 @@ UserDao provideUserDao(GithubDb db) {
RepoDao provideRepoDao(GithubDb db) {
return db.repoDao();
}
-
- @Singleton
- @Provides
- ViewModelProvider.Factory provideViewModelFactory(
- ViewModelSubComponent.Builder viewModelSubComponent) {
- return new GithubViewModelFactory(viewModelSubComponent.build());
- }
}
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelKey.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelKey.java
new file mode 100644
index 000000000..5ab7d9db5
--- /dev/null
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelKey.java
@@ -0,0 +1,19 @@
+package com.android.example.github.di;
+
+import android.arch.lifecycle.ViewModel;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import dagger.MapKey;
+
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@MapKey
+@interface ViewModelKey {
+ Class extends ViewModel> value();
+}
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelModule.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelModule.java
new file mode 100644
index 000000000..3c72fa0f6
--- /dev/null
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelModule.java
@@ -0,0 +1,34 @@
+package com.android.example.github.di;
+
+import android.arch.lifecycle.ViewModel;
+import android.arch.lifecycle.ViewModelProvider;
+
+import com.android.example.github.ui.repo.RepoViewModel;
+import com.android.example.github.ui.search.SearchViewModel;
+import com.android.example.github.ui.user.UserViewModel;
+import com.android.example.github.viewmodel.GithubViewModelFactory;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.multibindings.IntoMap;
+
+@Module
+abstract class ViewModelModule {
+ @Binds
+ @IntoMap
+ @ViewModelKey(UserViewModel.class)
+ abstract ViewModel bindUserViewModel(UserViewModel userViewModel);
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(SearchViewModel.class)
+ abstract ViewModel bindSearchViewModel(SearchViewModel searchViewModel);
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(RepoViewModel.class)
+ abstract ViewModel bindRepoViewModel(RepoViewModel repoViewModel);
+
+ @Binds
+ abstract ViewModelProvider.Factory bindViewModelFactory(GithubViewModelFactory factory);
+}
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelSubComponent.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelSubComponent.java
deleted file mode 100644
index 81cde2180..000000000
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelSubComponent.java
+++ /dev/null
@@ -1,39 +0,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.android.example.github.di;
-
-import com.android.example.github.ui.repo.RepoViewModel;
-import com.android.example.github.ui.search.SearchViewModel;
-import com.android.example.github.ui.user.UserViewModel;
-
-import dagger.Subcomponent;
-
-/**
- * A sub component to create ViewModels. It is called by the
- * {@link com.android.example.github.viewmodel.GithubViewModelFactory}. Using this component allows
- * ViewModels to define {@link javax.inject.Inject} constructors.
- */
-@Subcomponent
-public interface ViewModelSubComponent {
- @Subcomponent.Builder
- interface Builder {
- ViewModelSubComponent build();
- }
- UserViewModel userViewModel();
- SearchViewModel searchViewModel();
- RepoViewModel repoViewModel();
-}
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/NetworkBoundResource.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/NetworkBoundResource.java
index e1dc8c797..e7bde7491 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/NetworkBoundResource.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/NetworkBoundResource.java
@@ -18,7 +18,9 @@
import com.android.example.github.AppExecutors;
import com.android.example.github.api.ApiResponse;
+import com.android.example.github.util.Objects;
import com.android.example.github.vo.Resource;
+import com.android.example.github.vo.Status;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MediatorLiveData;
@@ -50,15 +52,22 @@ public abstract class NetworkBoundResource {
if (shouldFetch(data)) {
fetchFromNetwork(dbSource);
} else {
- result.addSource(dbSource, newData -> result.setValue(Resource.success(newData)));
+ result.addSource(dbSource, newData -> setValue(Resource.success(newData)));
}
});
}
+ @MainThread
+ private void setValue(Resource newValue) {
+ if (!Objects.equals(result.getValue(), newValue)) {
+ result.setValue(newValue);
+ }
+ }
+
private void fetchFromNetwork(final LiveData dbSource) {
LiveData> apiResponse = createCall();
// we re-attach dbSource as a new source, it will dispatch its latest value quickly
- result.addSource(dbSource, newData -> result.setValue(Resource.loading(newData)));
+ result.addSource(dbSource, newData -> setValue(Resource.loading(newData)));
result.addSource(apiResponse, response -> {
result.removeSource(apiResponse);
result.removeSource(dbSource);
@@ -71,13 +80,13 @@ private void fetchFromNetwork(final LiveData dbSource) {
// otherwise we will get immediately last cached value,
// which may not be updated with latest results received from network.
result.addSource(loadFromDb(),
- newData -> result.setValue(Resource.success(newData)))
+ newData -> setValue(Resource.success(newData)))
);
});
} else {
onFetchFailed();
result.addSource(dbSource,
- newData -> result.setValue(Resource.error(response.errorMessage, newData)));
+ newData -> setValue(Resource.error(response.errorMessage, newData)));
}
});
}
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/DataBoundListAdapter.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/DataBoundListAdapter.java
index 3e78b503a..ea9896755 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/DataBoundListAdapter.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/DataBoundListAdapter.java
@@ -31,7 +31,7 @@
* A generic RecyclerView adapter that uses Data Binding & DiffUtil.
*
* @param Type of the items in the list
- * @param The of the ViewDataBinding
+ * @param The type of the ViewDataBinding
*/
public abstract class DataBoundListAdapter
extends RecyclerView.Adapter> {
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoFragment.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoFragment.java
index 014231e9a..ad2e05bd5 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoFragment.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoFragment.java
@@ -46,14 +46,12 @@
/**
* The UI Controller for displaying a Github Repo's information with its contributors.
*/
-public class RepoFragment extends Fragment implements LifecycleRegistryOwner, Injectable {
+public class RepoFragment extends Fragment implements Injectable {
private static final String REPO_OWNER_KEY = "repo_owner";
private static final String REPO_NAME_KEY = "repo_name";
- private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
-
@Inject
ViewModelProvider.Factory viewModelFactory;
@@ -66,11 +64,6 @@ public class RepoFragment extends Fragment implements LifecycleRegistryOwner, In
AutoClearedValue binding;
AutoClearedValue adapter;
- @Override
- public LifecycleRegistry getLifecycle() {
- return lifecycleRegistry;
- }
-
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoViewModel.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoViewModel.java
index 4c980600e..3ef9a1122 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoViewModel.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoViewModel.java
@@ -73,7 +73,8 @@ public void retry() {
}
}
- void setId(String owner, String name) {
+ @VisibleForTesting
+ public void setId(String owner, String name) {
RepoId update = new RepoId(owner, name);
if (Objects.equals(repoId.getValue(), update)) {
return;
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchFragment.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchFragment.java
index 701fcaa4c..efd1b4289 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchFragment.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchFragment.java
@@ -24,7 +24,6 @@
import com.android.example.github.ui.common.RepoListAdapter;
import com.android.example.github.util.AutoClearedValue;
-import android.arch.lifecycle.LifecycleFragment;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
@@ -34,6 +33,7 @@
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
+import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -46,7 +46,7 @@
import javax.inject.Inject;
-public class SearchFragment extends LifecycleFragment implements Injectable {
+public class SearchFragment extends Fragment implements Injectable {
@Inject
ViewModelProvider.Factory viewModelFactory;
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchViewModel.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchViewModel.java
index b5f684bb6..490fe8dc2 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchViewModel.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchViewModel.java
@@ -56,7 +56,8 @@ public class SearchViewModel extends ViewModel {
});
}
- LiveData>> getResults() {
+ @VisibleForTesting
+ public LiveData>> getResults() {
return results;
}
@@ -69,11 +70,13 @@ public void setQuery(@NonNull String originalInput) {
query.setValue(input);
}
- LiveData getLoadMoreStatus() {
+ @VisibleForTesting
+ public LiveData getLoadMoreStatus() {
return nextPageHandler.getLoadMoreState();
}
- void loadNextPage() {
+ @VisibleForTesting
+ public void loadNextPage() {
String value = query.getValue();
if (value == null || value.trim().length() == 0) {
return;
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserFragment.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserFragment.java
index 33fca94b6..a993c55a6 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserFragment.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserFragment.java
@@ -16,28 +16,29 @@
package com.android.example.github.ui.user;
-import com.android.example.github.R;
-import com.android.example.github.binding.FragmentDataBindingComponent;
-import com.android.example.github.databinding.UserFragmentBinding;
-import com.android.example.github.di.Injectable;
-import com.android.example.github.ui.common.NavigationController;
-import com.android.example.github.ui.common.RepoListAdapter;
-import com.android.example.github.util.AutoClearedValue;
-
-import android.arch.lifecycle.LifecycleFragment;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.databinding.DataBindingComponent;
import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import com.android.example.github.R;
+import com.android.example.github.binding.FragmentDataBindingComponent;
+import com.android.example.github.databinding.UserFragmentBinding;
+import com.android.example.github.di.Injectable;
+import com.android.example.github.ui.common.NavigationController;
+import com.android.example.github.ui.common.RepoListAdapter;
+import com.android.example.github.util.AutoClearedValue;
+
import javax.inject.Inject;
-public class UserFragment extends LifecycleFragment implements Injectable {
+public class UserFragment extends Fragment implements Injectable {
private static final String LOGIN_KEY = "login";
@Inject
ViewModelProvider.Factory viewModelFactory;
@@ -46,7 +47,8 @@ public class UserFragment extends LifecycleFragment implements Injectable {
DataBindingComponent dataBindingComponent = new FragmentDataBindingComponent(this);
private UserViewModel userViewModel;
- private AutoClearedValue binding;
+ @VisibleForTesting
+ AutoClearedValue binding;
private AutoClearedValue adapter;
public static UserFragment create(String login) {
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserViewModel.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserViewModel.java
index 8a9a00e93..f7439d43d 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserViewModel.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserViewModel.java
@@ -58,22 +58,26 @@ public UserViewModel(UserRepository userRepository, RepoRepository repoRepositor
});
}
- void setLogin(String login) {
+ @VisibleForTesting
+ public void setLogin(String login) {
if (Objects.equals(this.login.getValue(), login)) {
return;
}
this.login.setValue(login);
}
- LiveData> getUser() {
+ @VisibleForTesting
+ public LiveData> getUser() {
return user;
}
- LiveData>> getRepositories() {
+ @VisibleForTesting
+ public LiveData>> getRepositories() {
return repositories;
}
- void retry() {
+ @VisibleForTesting
+ public void retry() {
if (this.login.getValue() != null) {
this.login.setValue(this.login.getValue());
}
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/util/AutoClearedValue.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/AutoClearedValue.java
index 001ce2ea9..257dbbda7 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/util/AutoClearedValue.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/AutoClearedValue.java
@@ -31,8 +31,10 @@ public AutoClearedValue(Fragment fragment, T value) {
new FragmentManager.FragmentLifecycleCallbacks() {
@Override
public void onFragmentViewDestroyed(FragmentManager fm, Fragment f) {
- AutoClearedValue.this.value = null;
- fragmentManager.unregisterFragmentLifecycleCallbacks(this);
+ if (f == fragment) {
+ AutoClearedValue.this.value = null;
+ fragmentManager.unregisterFragmentLifecycleCallbacks(this);
+ }
}
},false);
this.value = value;
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/util/LiveDataCallAdapter.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/LiveDataCallAdapter.java
index a22a553dd..299824d9e 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/util/LiveDataCallAdapter.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/LiveDataCallAdapter.java
@@ -30,7 +30,7 @@
import retrofit2.Response;
/**
- * A Retrofit adapterthat converts the Call into a LiveData of ApiResponse.
+ * A Retrofit adapter that converts the Call into a LiveData of ApiResponse.
* @param
*/
public class LiveDataCallAdapter implements CallAdapter>> {
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/viewmodel/GithubViewModelFactory.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/viewmodel/GithubViewModelFactory.java
index 64bbde8be..8b1a6a3fc 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/viewmodel/GithubViewModelFactory.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/viewmodel/GithubViewModelFactory.java
@@ -16,42 +16,30 @@
package com.android.example.github.viewmodel;
-import com.android.example.github.di.ViewModelSubComponent;
-import com.android.example.github.repository.RepoRepository;
-import com.android.example.github.repository.UserRepository;
-import com.android.example.github.ui.repo.RepoViewModel;
-import com.android.example.github.ui.search.SearchViewModel;
-import com.android.example.github.ui.user.UserViewModel;
-
import android.arch.lifecycle.ViewModel;
import android.arch.lifecycle.ViewModelProvider;
-import android.support.v4.util.ArrayMap;
import java.util.Map;
-import java.util.concurrent.Callable;
import javax.inject.Inject;
+import javax.inject.Provider;
import javax.inject.Singleton;
@Singleton
public class GithubViewModelFactory implements ViewModelProvider.Factory {
- private final ArrayMap> creators;
+ private final Map, Provider> creators;
@Inject
- public GithubViewModelFactory(ViewModelSubComponent viewModelSubComponent) {
- creators = new ArrayMap<>();
- // we cannot inject view models directly because they won't be bound to the owner's
- // view model scope.
- creators.put(SearchViewModel.class, () -> viewModelSubComponent.searchViewModel());
- creators.put(UserViewModel.class, () -> viewModelSubComponent.userViewModel());
- creators.put(RepoViewModel.class, () -> viewModelSubComponent.repoViewModel());
+ public GithubViewModelFactory(Map, Provider> creators) {
+ this.creators = creators;
}
+ @SuppressWarnings("unchecked")
@Override
public T create(Class modelClass) {
- Callable extends ViewModel> creator = creators.get(modelClass);
+ Provider extends ViewModel> creator = creators.get(modelClass);
if (creator == null) {
- for (Map.Entry> entry : creators.entrySet()) {
+ for (Map.Entry, Provider> entry : creators.entrySet()) {
if (modelClass.isAssignableFrom(entry.getKey())) {
creator = entry.getValue();
break;
@@ -62,7 +50,7 @@ public T create(Class modelClass) {
throw new IllegalArgumentException("unknown model class " + modelClass);
}
try {
- return (T) creator.call();
+ return (T) creator.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Contributor.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Contributor.java
index d8f1a0dae..6bda8491c 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Contributor.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Contributor.java
@@ -20,6 +20,7 @@
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
+import android.support.annotation.NonNull;
@Entity(primaryKeys = {"repoName", "repoOwner", "login"},
foreignKeys = @ForeignKey(entity = Repo.class,
@@ -30,6 +31,7 @@
public class Contributor {
@SerializedName("login")
+ @NonNull
private final String login;
@SerializedName("contributions")
@@ -38,8 +40,10 @@ public class Contributor {
@SerializedName("avatar_url")
private final String avatarUrl;
+ @NonNull
private String repoName;
+ @NonNull
private String repoOwner;
public Contributor(String login, int contributions, String avatarUrl) {
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Repo.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Repo.java
index 1673d10a6..e1fab93cf 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Repo.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Repo.java
@@ -21,6 +21,7 @@
import android.arch.persistence.room.Embedded;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Index;
+import android.support.annotation.NonNull;
import javax.inject.Inject;
@@ -34,6 +35,7 @@ public class Repo {
public static final int UNKNOWN_ID = -1;
public final int id;
@SerializedName("name")
+ @NonNull
public final String name;
@SerializedName("full_name")
public final String fullName;
@@ -43,6 +45,7 @@ public class Repo {
public final int stars;
@SerializedName("owner")
@Embedded(prefix = "owner_")
+ @NonNull
public final Owner owner;
public Repo(int id, String name, String fullName, String description, Owner owner, int stars) {
@@ -56,11 +59,12 @@ public Repo(int id, String name, String fullName, String description, Owner owne
public static class Owner {
@SerializedName("login")
+ @NonNull
public final String login;
@SerializedName("url")
public final String url;
- public Owner(String login, String url) {
+ public Owner(@NonNull String login, String url) {
this.login = login;
this.url = url;
}
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/RepoSearchResult.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/RepoSearchResult.java
index 2b620df1b..23755ec82 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/RepoSearchResult.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/RepoSearchResult.java
@@ -20,6 +20,7 @@
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.TypeConverters;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.List;
@@ -27,14 +28,15 @@
@Entity(primaryKeys = {"query"})
@TypeConverters(GithubTypeConverters.class)
public class RepoSearchResult {
+ @NonNull
public final String query;
public final List repoIds;
public final int totalCount;
@Nullable
public final Integer next;
- public RepoSearchResult(String query, List repoIds, int totalCount,
- Integer next) {
+ public RepoSearchResult(@NonNull String query, List repoIds, int totalCount,
+ @Nullable Integer next) {
this.query = query;
this.repoIds = repoIds;
this.totalCount = totalCount;
diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/User.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/User.java
index 398d96c12..66a3eec06 100644
--- a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/User.java
+++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/User.java
@@ -18,11 +18,14 @@
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Index;
+import android.support.annotation.NonNull;
+
import com.google.gson.annotations.SerializedName;
@Entity(primaryKeys = "login")
public class User {
@SerializedName("login")
+ @NonNull
public final String login;
@SerializedName("avatar_url")
public final String avatarUrl;
diff --git a/GithubBrowserSample/app/src/main/res/layout/search_fragment.xml b/GithubBrowserSample/app/src/main/res/layout/search_fragment.xml
index f737533a1..d39e6160f 100644
--- a/GithubBrowserSample/app/src/main/res/layout/search_fragment.xml
+++ b/GithubBrowserSample/app/src/main/res/layout/search_fragment.xml
@@ -26,8 +26,7 @@
-
+
+
+
+
+
+
+
+
+
+
diff --git a/GithubBrowserSample/versions.gradle b/GithubBrowserSample/versions.gradle
new file mode 100644
index 000000000..85822d50b
--- /dev/null
+++ b/GithubBrowserSample/versions.gradle
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+
+
+/**
+* Shared file between builds so that they can all use the same dependencies and
+* maven repositories.
+**/
+ext.deps = [:]
+def versions = [:]
+versions.arch_core = "1.1.1"
+versions.room = "1.1.0-beta2"
+versions.lifecycle = "1.1.1"
+versions.support = "27.1.0"
+versions.dagger = "2.11"
+versions.junit = "4.12"
+versions.espresso = "3.0.1"
+versions.retrofit = "2.3.0"
+versions.okhttp_logging_interceptor = "3.9.0"
+versions.mockwebserver = "3.8.1"
+versions.apache_commons = "2.5"
+versions.mockito = "2.7.19"
+versions.mockito_all = "1.10.19"
+versions.dexmaker = "2.2.0"
+versions.constraint_layout = "1.0.2"
+versions.glide = "3.8.0"
+versions.timber = "4.5.1"
+versions.android_gradle_plugin = "3.0.1"
+versions.rxjava2 = "2.1.3"
+versions.rx_android = "2.0.1"
+versions.atsl_runner = "1.0.1"
+versions.atsl_rules = "1.0.1"
+versions.hamcrest = "1.3"
+versions.kotlin = "1.2.20"
+versions.paging = "1.0.0-beta1"
+def deps = [:]
+
+def support = [:]
+support.annotations = "com.android.support:support-annotations:$versions.support"
+support.app_compat = "com.android.support:appcompat-v7:$versions.support"
+support.recyclerview = "com.android.support:recyclerview-v7:$versions.support"
+support.cardview = "com.android.support:cardview-v7:$versions.support"
+support.design = "com.android.support:design:$versions.support"
+support.v4 = "com.android.support:support-v4:$versions.support"
+support.core_utils = "com.android.support:support-core-utils:$versions.support"
+deps.support = support
+
+def room = [:]
+room.runtime = "android.arch.persistence.room:runtime:$versions.room"
+room.compiler = "android.arch.persistence.room:compiler:$versions.room"
+room.rxjava2 = "android.arch.persistence.room:rxjava2:$versions.room"
+room.testing = "android.arch.persistence.room:testing:$versions.room"
+deps.room = room
+
+def lifecycle = [:]
+lifecycle.runtime = "android.arch.lifecycle:runtime:$versions.lifecycle"
+lifecycle.extensions = "android.arch.lifecycle:extensions:$versions.lifecycle"
+lifecycle.java8 = "android.arch.lifecycle:common-java8:$versions.lifecycle"
+lifecycle.compiler = "android.arch.lifecycle:compiler:$versions.lifecycle"
+deps.lifecycle = lifecycle
+
+def arch_core = [:]
+arch_core.testing = "android.arch.core:core-testing:$versions.arch_core"
+deps.arch_core = arch_core
+
+def retrofit = [:]
+retrofit.runtime = "com.squareup.retrofit2:retrofit:$versions.retrofit"
+retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit"
+retrofit.mock = "com.squareup.retrofit2:retrofit-mock:$versions.retrofit"
+deps.retrofit = retrofit
+deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:${versions.okhttp_logging_interceptor}"
+
+def dagger = [:]
+dagger.runtime = "com.google.dagger:dagger:$versions.dagger"
+dagger.android = "com.google.dagger:dagger-android:$versions.dagger"
+dagger.android_support = "com.google.dagger:dagger-android-support:$versions.dagger"
+dagger.compiler = "com.google.dagger:dagger-compiler:$versions.dagger"
+dagger.android_support_compiler = "com.google.dagger:dagger-android-processor:$versions.dagger"
+
+deps.dagger = dagger
+
+def espresso = [:]
+espresso.core = "com.android.support.test.espresso:espresso-core:$versions.espresso"
+espresso.contrib = "com.android.support.test.espresso:espresso-contrib:$versions.espresso"
+espresso.intents = "com.android.support.test.espresso:espresso-intents:$versions.espresso"
+deps.espresso = espresso
+
+def atsl = [:]
+atsl.runner = "com.android.support.test:runner:$versions.atsl_runner"
+atsl.rules = "com.android.support.test:rules:$versions.atsl_runner"
+deps.atsl = atsl
+
+def mockito = [:]
+mockito.core = "org.mockito:mockito-core:$versions.mockito"
+mockito.all = "org.mockito:mockito-all:$versions.mockito_all"
+deps.mockito = mockito
+
+def kotlin = [:]
+kotlin.stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jre7:$versions.kotlin"
+kotlin.test = "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin"
+kotlin.plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
+
+deps.kotlin = kotlin
+deps.paging = "android.arch.paging:runtime:$versions.paging"
+deps.glide = "com.github.bumptech.glide:glide:$versions.glide"
+deps.dexmaker = "com.linkedin.dexmaker:dexmaker-mockito:$versions.dexmaker"
+deps.constraint_layout = "com.android.support.constraint:constraint-layout:$versions.constraint_layout"
+deps.timber = "com.jakewharton.timber:timber:$versions.timber"
+deps.junit = "junit:junit:$versions.junit"
+deps.mock_web_server = "com.squareup.okhttp3:mockwebserver:$versions.mockwebserver"
+deps.rxjava2 = "io.reactivex.rxjava2:rxjava:$versions.rxjava2"
+deps.rx_android = "io.reactivex.rxjava2:rxandroid:$versions.rx_android"
+deps.hamcrest = "org.hamcrest:hamcrest-all:$versions.hamcrest"
+deps.android_gradle_plugin = "com.android.tools.build:gradle:$versions.android_gradle_plugin"
+ext.deps = deps
+
+def build_versions = [:]
+build_versions.min_sdk = 14
+build_versions.target_sdk = 26
+build_versions.build_tools = "27.0.3"
+ext.build_versions = build_versions
+
+
+def addRepos(RepositoryHandler handler) {
+ handler.google()
+ handler.jcenter()
+ handler.maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
+}
+ext.addRepos = this.&addRepos
diff --git a/PagingSample/.gitignore b/PagingSample/.gitignore
new file mode 100644
index 000000000..39fb081a4
--- /dev/null
+++ b/PagingSample/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/PagingSample/.google/packaging.yaml b/PagingSample/.google/packaging.yaml
new file mode 100644
index 000000000..108903dd1
--- /dev/null
+++ b/PagingSample/.google/packaging.yaml
@@ -0,0 +1,41 @@
+#
+# 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.
+
+# GOOGLE SAMPLE PACKAGING DATA
+#
+# This file is used by Google as part of our samples packaging process.
+# End users may safely ignore this file. It has no relevance to other systems.
+---
+# Values: {DRAFT | PUBLISHED | INTERNAL | DEPRECATED | SUPERCEDED}
+status: PUBLISHED
+
+# Optional, put additional explanation here for DEPRECATED or SUPERCEDED.
+# statusNote:
+
+# See http://go/sample-categories
+technologies: [Android]
+categories: [Architecture]
+languages: [Java]
+solutions: [Mobile]
+
+# May be omitted if unpublished
+github: googlesamples/android-architecture-components
+
+# Values: BEGINNER | INTERMEDIATE | ADVANCED | EXPERT
+level: ADVANCED
+
+# Default: apache2. May be omitted for most samples.
+# Alternatives: apache2-android (for AOSP)
+license: apache2
diff --git a/PagingSample/README.md b/PagingSample/README.md
new file mode 100644
index 000000000..d975917d2
--- /dev/null
+++ b/PagingSample/README.md
@@ -0,0 +1,36 @@
+Android Architecture Components Paging Sample
+==============================================
+
+This sample showcases the following Architecture Components:
+
+* [Paging](https://developer.android.com/topic/libraries/architecture/paging.html)
+* [Room](https://developer.android.com/topic/libraries/architecture/room.html)
+* [ViewModels](https://developer.android.com/reference/android/arch/lifecycle/ViewModel.html)
+* [LiveData](https://developer.android.com/reference/android/arch/lifecycle/LiveData.html)
+
+Introduction
+-------------
+
+### Features
+
+This sample contains a single screen with a list of text items. Items can be added to the list with the input at the top, and swiping items in the list removes them.
+
+License
+-------
+
+Copyright 2017 The Android Open Source Project, Inc.
+
+Licensed to the Apache Software Foundation (ASF) under one or more contributor
+license agreements. See the NOTICE file distributed with this work for
+additional information regarding copyright ownership. The ASF licenses this
+file to you 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.
diff --git a/PagingSample/app/.gitignore b/PagingSample/app/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/PagingSample/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/PagingSample/app/build.gradle b/PagingSample/app/build.gradle
new file mode 100644
index 000000000..4b56959b1
--- /dev/null
+++ b/PagingSample/app/build.gradle
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+apply plugin: 'com.android.application'
+
+apply plugin: 'kotlin-android'
+
+apply plugin: 'kotlin-android-extensions'
+
+apply plugin: 'kotlin-kapt'
+android {
+ compileSdkVersion build_versions.target_sdk
+ buildToolsVersion build_versions.build_tools
+ defaultConfig {
+ applicationId "paging.android.example.com.pagingsample"
+ minSdkVersion build_versions.min_sdk
+ targetSdkVersion build_versions.target_sdk
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation deps.support.app_compat
+ implementation deps.support.recyclerview
+ implementation deps.support.cardview
+ implementation deps.room.runtime
+ implementation deps.lifecycle.runtime
+ implementation deps.lifecycle.extensions
+ implementation deps.paging
+ implementation deps.kotlin.stdlib
+
+ // Android Testing Support Library's runner and rules
+ androidTestImplementation deps.atsl.runner
+ androidTestImplementation deps.atsl.rules
+ androidTestImplementation deps.room.testing
+ androidTestImplementation deps.arch_core.testing
+
+ kapt deps.room.compiler
+}
\ No newline at end of file
diff --git a/PagingSample/app/proguard-rules.pro b/PagingSample/app/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/PagingSample/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/PagingSample/app/src/androidTest/java/paging/android/example/com/pagingsample/MainActivityTest.java b/PagingSample/app/src/androidTest/java/paging/android/example/com/pagingsample/MainActivityTest.java
new file mode 100644
index 000000000..7430df3e7
--- /dev/null
+++ b/PagingSample/app/src/androidTest/java/paging/android/example/com/pagingsample/MainActivityTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 paging.android.example.com.pagingsample;
+
+import android.app.Activity;
+import android.arch.core.executor.testing.CountingTaskExecutorRule;
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Simply sanity test to ensure that activity launches without any issues and shows some data.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MainActivityTest {
+ @Rule
+ public CountingTaskExecutorRule testRule = new CountingTaskExecutorRule();
+
+ @Test
+ public void showSomeResults() throws InterruptedException, TimeoutException {
+ Intent intent = new Intent(InstrumentationRegistry.getTargetContext(), MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ Activity activity = InstrumentationRegistry.getInstrumentation().startActivitySync(intent);
+ testRule.drainTasks(10, TimeUnit.SECONDS);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ final RecyclerView recyclerView = activity.findViewById(R.id.cheeseList);
+ waitForAdapterChange(recyclerView);
+ assertThat(recyclerView.getAdapter(), notNullValue());
+ waitForAdapterChange(recyclerView);
+ assertThat(recyclerView.getAdapter().getItemCount() > 0, is(true));
+ }
+
+ private void waitForAdapterChange(final RecyclerView recyclerView) throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ recyclerView.getAdapter().registerAdapterDataObserver(
+ new RecyclerView.AdapterDataObserver() {
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ latch.countDown();
+ }
+
+ @Override
+ public void onChanged() {
+ latch.countDown();
+ }
+ });
+ }
+ });
+ if (recyclerView.getAdapter().getItemCount() > 0) {
+ return;//already loaded
+ }
+ assertThat(latch.await(10, TimeUnit.SECONDS), is(true));
+ }
+}
diff --git a/PagingSample/app/src/main/AndroidManifest.xml b/PagingSample/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..415782e73
--- /dev/null
+++ b/PagingSample/app/src/main/AndroidManifest.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/Cheese.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/Cheese.kt
new file mode 100644
index 000000000..f620e4106
--- /dev/null
+++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/Cheese.kt
@@ -0,0 +1,26 @@
+/*
+ * 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 paging.android.example.com.pagingsample
+
+import android.arch.persistence.room.Entity
+import android.arch.persistence.room.PrimaryKey
+
+/**
+ * Data class that represents our items.
+ */
+@Entity
+data class Cheese(@PrimaryKey(autoGenerate = true) val id: Int, val name: String)
\ No newline at end of file
diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseAdapter.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseAdapter.kt
new file mode 100644
index 000000000..8c3b7f84e
--- /dev/null
+++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseAdapter.kt
@@ -0,0 +1,68 @@
+/*
+ * 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 paging.android.example.com.pagingsample
+
+import android.arch.paging.PagedListAdapter
+import android.support.v7.util.DiffUtil
+import android.view.ViewGroup
+
+/**
+ * A simple PagedListAdapter that binds Cheese items into CardViews.
+ *
+ * PagedListAdapter is a RecyclerView.Adapter base class which can present the content of PagedLists
+ * in a RecyclerView. It requests new pages as the user scrolls, and handles new PagedLists by
+ * computing list differences on a background thread, and dispatching minimal, efficient updates to
+ * the RecyclerView to ensure minimal UI thread work.
+ *
+ * If you want to use your own Adapter base class, try using a PagedListAdapterHelper inside your
+ * adapter instead.
+ *
+ * @see android.arch.paging.PagedListAdapter
+ * @see android.arch.paging.AsyncPagedListDiffer
+ */
+class CheeseAdapter : PagedListAdapter(diffCallback) {
+ override fun onBindViewHolder(holder: CheeseViewHolder, position: Int) {
+ holder.bindTo(getItem(position))
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheeseViewHolder =
+ CheeseViewHolder(parent)
+
+ companion object {
+ /**
+ * This diff callback informs the PagedListAdapter how to compute list differences when new
+ * PagedLists arrive.
+ *
+ * When you add a Cheese with the 'Add' button, the PagedListAdapter uses diffCallback to
+ * detect there's only a single item difference from before, so it only needs to animate and
+ * rebind a single view.
+ *
+ * @see android.support.v7.util.DiffUtil
+ */
+ private val diffCallback = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
+ oldItem.id == newItem.id
+
+ /**
+ * Note that in kotlin, == checking on data classes compares all contents, but in Java,
+ * typically you'll implement Object#equals, and use it to compare object contents.
+ */
+ override fun areContentsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
+ oldItem == newItem
+ }
+ }
+}
diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseDao.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseDao.kt
new file mode 100644
index 000000000..7b677c4e5
--- /dev/null
+++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseDao.kt
@@ -0,0 +1,45 @@
+/*
+ * 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 paging.android.example.com.pagingsample
+
+import android.arch.paging.DataSource
+import android.arch.persistence.room.Dao
+import android.arch.persistence.room.Delete
+import android.arch.persistence.room.Insert
+import android.arch.persistence.room.Query
+
+/**
+ * Database Access Object for the Cheese database.
+ */
+@Dao
+interface CheeseDao {
+ /**
+ * Room knows how to return a LivePagedListProvider, from which we can get a LiveData and serve
+ * it back to UI via ViewModel.
+ */
+ @Query("SELECT * FROM Cheese ORDER BY name COLLATE NOCASE ASC")
+ fun allCheesesByName(): DataSource.Factory
+
+ @Insert
+ fun insert(cheeses: List)
+
+ @Insert
+ fun insert(cheese: Cheese)
+
+ @Delete
+ fun delete(cheese: Cheese)
+}
\ No newline at end of file
diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseDb.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseDb.kt
new file mode 100644
index 000000000..d15517b8e
--- /dev/null
+++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseDb.kt
@@ -0,0 +1,191 @@
+/*
+ * 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 paging.android.example.com.pagingsample
+
+import android.arch.persistence.db.SupportSQLiteDatabase
+import android.arch.persistence.room.*
+import android.content.Context
+
+/**
+ * Singleton database object. Note that for a real app, you should probably use a Dependency
+ * Injection framework or Service Locator to create the singleton database.
+ */
+@Database(entities = arrayOf(Cheese::class), version = 1)
+abstract class CheeseDb : RoomDatabase() {
+ abstract fun cheeseDao(): CheeseDao
+
+ companion object {
+ private var instance: CheeseDb? = null
+ @Synchronized
+ fun get(context: Context): CheeseDb {
+ if (instance == null) {
+ instance = Room.databaseBuilder(context.applicationContext,
+ CheeseDb::class.java, "CheeseDatabase")
+ .addCallback(object : RoomDatabase.Callback() {
+ override fun onCreate(db: SupportSQLiteDatabase) {
+ fillInDb(context.applicationContext)
+ }
+ }).build()
+ }
+ return instance!!
+ }
+
+ /**
+ * fill database with list of cheeses
+ */
+ private fun fillInDb(context: Context) {
+ // inserts in Room are executed on the current thread, so we insert in the background
+ ioThread {
+ get(context).cheeseDao().insert(
+ CHEESE_DATA.map { Cheese(id = 0, name = it) })
+ }
+ }
+ }
+}
+
+
+private val CHEESE_DATA = arrayListOf(
+ "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
+ "Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale",
+ "Aisy Cendre", "Allgauer Emmentaler", "Alverca", "Ambert", "American Cheese",
+ "Ami du Chambertin", "Anejo Enchilado", "Anneau du Vic-Bilh", "Anthoriro", "Appenzell",
+ "Aragon", "Ardi Gasna", "Ardrahan", "Armenian String", "Aromes au Gene de Marc",
+ "Asadero", "Asiago", "Aubisque Pyrenees", "Autun", "Avaxtskyr", "Baby Swiss",
+ "Babybel", "Baguette Laonnaise", "Bakers", "Baladi", "Balaton", "Bandal", "Banon",
+ "Barry's Bay Cheddar", "Basing", "Basket Cheese", "Bath Cheese", "Bavarian Bergkase",
+ "Baylough", "Beaufort", "Beauvoorde", "Beenleigh Blue", "Beer Cheese", "Bel Paese",
+ "Bergader", "Bergere Bleue", "Berkswell", "Beyaz Peynir", "Bierkase", "Bishop Kennedy",
+ "Blarney", "Bleu d'Auvergne", "Bleu de Gex", "Bleu de Laqueuille",
+ "Bleu de Septmoncel", "Bleu Des Causses", "Blue", "Blue Castello", "Blue Rathgore",
+ "Blue Vein (Australian)", "Blue Vein Cheeses", "Bocconcini", "Bocconcini (Australian)",
+ "Boeren Leidenkaas", "Bonchester", "Bosworth", "Bougon", "Boule Du Roves",
+ "Boulette d'Avesnes", "Boursault", "Boursin", "Bouyssou", "Bra", "Braudostur",
+ "Breakfast Cheese", "Brebis du Lavort", "Brebis du Lochois", "Brebis du Puyfaucon",
+ "Bresse Bleu", "Brick", "Brie", "Brie de Meaux", "Brie de Melun", "Brillat-Savarin",
+ "Brin", "Brin d' Amour", "Brin d'Amour", "Brinza (Burduf Brinza)",
+ "Briquette de Brebis", "Briquette du Forez", "Broccio", "Broccio Demi-Affine",
+ "Brousse du Rove", "Bruder Basil", "Brusselae Kaas (Fromage de Bruxelles)", "Bryndza",
+ "Buchette d'Anjou", "Buffalo", "Burgos", "Butte", "Butterkase", "Button (Innes)",
+ "Buxton Blue", "Cabecou", "Caboc", "Cabrales", "Cachaille", "Caciocavallo", "Caciotta",
+ "Caerphilly", "Cairnsmore", "Calenzana", "Cambazola", "Camembert de Normandie",
+ "Canadian Cheddar", "Canestrato", "Cantal", "Caprice des Dieux", "Capricorn Goat",
+ "Capriole Banon", "Carre de l'Est", "Casciotta di Urbino", "Cashel Blue", "Castellano",
+ "Castelleno", "Castelmagno", "Castelo Branco", "Castigliano", "Cathelain",
+ "Celtic Promise", "Cendre d'Olivet", "Cerney", "Chabichou", "Chabichou du Poitou",
+ "Chabis de Gatine", "Chaource", "Charolais", "Chaumes", "Cheddar",
+ "Cheddar Clothbound", "Cheshire", "Chevres", "Chevrotin des Aravis", "Chontaleno",
+ "Civray", "Coeur de Camembert au Calvados", "Coeur de Chevre", "Colby", "Cold Pack",
+ "Comte", "Coolea", "Cooleney", "Coquetdale", "Corleggy", "Cornish Pepper",
+ "Cotherstone", "Cotija", "Cottage Cheese", "Cottage Cheese (Australian)",
+ "Cougar Gold", "Coulommiers", "Coverdale", "Crayeux de Roncq", "Cream Cheese",
+ "Cream Havarti", "Crema Agria", "Crema Mexicana", "Creme Fraiche", "Crescenza",
+ "Croghan", "Crottin de Chavignol", "Crottin du Chavignol", "Crowdie", "Crowley",
+ "Cuajada", "Curd", "Cure Nantais", "Curworthy", "Cwmtawe Pecorino",
+ "Cypress Grove Chevre", "Danablu (Danish Blue)", "Danbo", "Danish Fontina",
+ "Daralagjazsky", "Dauphin", "Delice des Fiouves", "Denhany Dorset Drum", "Derby",
+ "Dessertnyj Belyj", "Devon Blue", "Devon Garland", "Dolcelatte", "Doolin",
+ "Doppelrhamstufel", "Dorset Blue Vinney", "Double Gloucester", "Double Worcester",
+ "Dreux a la Feuille", "Dry Jack", "Duddleswell", "Dunbarra", "Dunlop", "Dunsyre Blue",
+ "Duroblando", "Durrus", "Dutch Mimolette (Commissiekaas)", "Edam", "Edelpilz",
+ "Emental Grand Cru", "Emlett", "Emmental", "Epoisses de Bourgogne", "Esbareich",
+ "Esrom", "Etorki", "Evansdale Farmhouse Brie", "Evora De L'Alentejo", "Exmoor Blue",
+ "Explorateur", "Feta", "Feta (Australian)", "Figue", "Filetta", "Fin-de-Siecle",
+ "Finlandia Swiss", "Finn", "Fiore Sardo", "Fleur du Maquis", "Flor de Guia",
+ "Flower Marie", "Folded", "Folded cheese with mint", "Fondant de Brebis",
+ "Fontainebleau", "Fontal", "Fontina Val d'Aosta", "Formaggio di capra", "Fougerus",
+ "Four Herb Gouda", "Fourme d' Ambert", "Fourme de Haute Loire", "Fourme de Montbrison",
+ "Fresh Jack", "Fresh Mozzarella", "Fresh Ricotta", "Fresh Truffles", "Fribourgeois",
+ "Friesekaas", "Friesian", "Friesla", "Frinault", "Fromage a Raclette", "Fromage Corse",
+ "Fromage de Montagne de Savoie", "Fromage Frais", "Fruit Cream Cheese",
+ "Frying Cheese", "Fynbo", "Gabriel", "Galette du Paludier", "Galette Lyonnaise",
+ "Galloway Goat's Milk Gems", "Gammelost", "Gaperon a l'Ail", "Garrotxa", "Gastanberra",
+ "Geitost", "Gippsland Blue", "Gjetost", "Gloucester", "Golden Cross", "Gorgonzola",
+ "Gornyaltajski", "Gospel Green", "Gouda", "Goutu", "Gowrie", "Grabetto", "Graddost",
+ "Grafton Village Cheddar", "Grana", "Grana Padano", "Grand Vatel",
+ "Grataron d' Areches", "Gratte-Paille", "Graviera", "Greuilh", "Greve",
+ "Gris de Lille", "Gruyere", "Gubbeen", "Guerbigny", "Halloumi",
+ "Halloumy (Australian)", "Haloumi-Style Cheese", "Harbourne Blue", "Havarti",
+ "Heidi Gruyere", "Hereford Hop", "Herrgardsost", "Herriot Farmhouse", "Herve",
+ "Hipi Iti", "Hubbardston Blue Cow", "Hushallsost", "Iberico", "Idaho Goatster",
+ "Idiazabal", "Il Boschetto al Tartufo", "Ile d'Yeu", "Isle of Mull", "Jarlsberg",
+ "Jermi Tortes", "Jibneh Arabieh", "Jindi Brie", "Jubilee Blue", "Juustoleipa",
+ "Kadchgall", "Kaseri", "Kashta", "Kefalotyri", "Kenafa", "Kernhem", "Kervella Affine",
+ "Kikorangi", "King Island Cape Wickham Brie", "King River Gold", "Klosterkaese",
+ "Knockalara", "Kugelkase", "L'Aveyronnais", "L'Ecir de l'Aubrac", "La Taupiniere",
+ "La Vache Qui Rit", "Laguiole", "Lairobell", "Lajta", "Lanark Blue", "Lancashire",
+ "Langres", "Lappi", "Laruns", "Lavistown", "Le Brin", "Le Fium Orbo", "Le Lacandou",
+ "Le Roule", "Leafield", "Lebbene", "Leerdammer", "Leicester", "Leyden", "Limburger",
+ "Lincolnshire Poacher", "Lingot Saint Bousquet d'Orb", "Liptauer", "Little Rydings",
+ "Livarot", "Llanboidy", "Llanglofan Farmhouse", "Loch Arthur Farmhouse",
+ "Loddiswell Avondale", "Longhorn", "Lou Palou", "Lou Pevre", "Lyonnais", "Maasdam",
+ "Macconais", "Mahoe Aged Gouda", "Mahon", "Malvern", "Mamirolle", "Manchego",
+ "Manouri", "Manur", "Marble Cheddar", "Marbled Cheeses", "Maredsous", "Margotin",
+ "Maribo", "Maroilles", "Mascares", "Mascarpone", "Mascarpone (Australian)",
+ "Mascarpone Torta", "Matocq", "Maytag Blue", "Meira", "Menallack Farmhouse",
+ "Menonita", "Meredith Blue", "Mesost", "Metton (Cancoillotte)", "Meyer Vintage Gouda",
+ "Mihalic Peynir", "Milleens", "Mimolette", "Mine-Gabhar", "Mini Baby Bells", "Mixte",
+ "Molbo", "Monastery Cheeses", "Mondseer", "Mont D'or Lyonnais", "Montasio",
+ "Monterey Jack", "Monterey Jack Dry", "Morbier", "Morbier Cru de Montagne",
+ "Mothais a la Feuille", "Mozzarella", "Mozzarella (Australian)",
+ "Mozzarella di Bufala", "Mozzarella Fresh, in water", "Mozzarella Rolls", "Munster",
+ "Murol", "Mycella", "Myzithra", "Naboulsi", "Nantais", "Neufchatel",
+ "Neufchatel (Australian)", "Niolo", "Nokkelost", "Northumberland", "Oaxaca",
+ "Olde York", "Olivet au Foin", "Olivet Bleu", "Olivet Cendre",
+ "Orkney Extra Mature Cheddar", "Orla", "Oschtjepka", "Ossau Fermier", "Ossau-Iraty",
+ "Oszczypek", "Oxford Blue", "P'tit Berrichon", "Palet de Babligny", "Paneer", "Panela",
+ "Pannerone", "Pant ys Gawn", "Parmesan (Parmigiano)", "Parmigiano Reggiano",
+ "Pas de l'Escalette", "Passendale", "Pasteurized Processed", "Pate de Fromage",
+ "Patefine Fort", "Pave d'Affinois", "Pave d'Auge", "Pave de Chirac", "Pave du Berry",
+ "Pecorino", "Pecorino in Walnut Leaves", "Pecorino Romano", "Peekskill Pyramid",
+ "Pelardon des Cevennes", "Pelardon des Corbieres", "Penamellera", "Penbryn",
+ "Pencarreg", "Perail de Brebis", "Petit Morin", "Petit Pardou", "Petit-Suisse",
+ "Picodon de Chevre", "Picos de Europa", "Piora", "Pithtviers au Foin",
+ "Plateau de Herve", "Plymouth Cheese", "Podhalanski", "Poivre d'Ane", "Polkolbin",
+ "Pont l'Eveque", "Port Nicholson", "Port-Salut", "Postel", "Pouligny-Saint-Pierre",
+ "Pourly", "Prastost", "Pressato", "Prince-Jean", "Processed Cheddar", "Provolone",
+ "Provolone (Australian)", "Pyengana Cheddar", "Pyramide", "Quark",
+ "Quark (Australian)", "Quartirolo Lombardo", "Quatre-Vents", "Quercy Petit",
+ "Queso Blanco", "Queso Blanco con Frutas --Pina y Mango", "Queso de Murcia",
+ "Queso del Montsec", "Queso del Tietar", "Queso Fresco", "Queso Fresco (Adobera)",
+ "Queso Iberico", "Queso Jalapeno", "Queso Majorero", "Queso Media Luna",
+ "Queso Para Frier", "Queso Quesadilla", "Rabacal", "Raclette", "Ragusano", "Raschera",
+ "Reblochon", "Red Leicester", "Regal de la Dombes", "Reggianito", "Remedou",
+ "Requeson", "Richelieu", "Ricotta", "Ricotta (Australian)", "Ricotta Salata", "Ridder",
+ "Rigotte", "Rocamadour", "Rollot", "Romano", "Romans Part Dieu", "Roncal", "Roquefort",
+ "Roule", "Rouleau De Beaulieu", "Royalp Tilsit", "Rubens", "Rustinu", "Saaland Pfarr",
+ "Saanenkaese", "Saga", "Sage Derby", "Sainte Maure", "Saint-Marcellin",
+ "Saint-Nectaire", "Saint-Paulin", "Salers", "Samso", "San Simon", "Sancerre",
+ "Sap Sago", "Sardo", "Sardo Egyptian", "Sbrinz", "Scamorza", "Schabzieger", "Schloss",
+ "Selles sur Cher", "Selva", "Serat", "Seriously Strong Cheddar", "Serra da Estrela",
+ "Sharpam", "Shelburne Cheddar", "Shropshire Blue", "Siraz", "Sirene", "Smoked Gouda",
+ "Somerset Brie", "Sonoma Jack", "Sottocenare al Tartufo", "Soumaintrain",
+ "Sourire Lozerien", "Spenwood", "Sraffordshire Organic", "St. Agur Blue Cheese",
+ "Stilton", "Stinking Bishop", "String", "Sussex Slipcote", "Sveciaost", "Swaledale",
+ "Sweet Style Swiss", "Swiss", "Syrian (Armenian String)", "Tala", "Taleggio", "Tamie",
+ "Tasmania Highland Chevre Log", "Taupiniere", "Teifi", "Telemea", "Testouri",
+ "Tete de Moine", "Tetilla", "Texas Goat Cheese", "Tibet", "Tillamook Cheddar",
+ "Tilsit", "Timboon Brie", "Toma", "Tomme Brulee", "Tomme d'Abondance",
+ "Tomme de Chevre", "Tomme de Romans", "Tomme de Savoie", "Tomme des Chouans", "Tommes",
+ "Torta del Casar", "Toscanello", "Touree de L'Aubier", "Tourmalet",
+ "Trappe (Veritable)", "Trois Cornes De Vendee", "Tronchon", "Trou du Cru", "Truffe",
+ "Tupi", "Turunmaa", "Tymsboro", "Tyn Grug", "Tyning", "Ubriaco", "Ulloa",
+ "Vacherin-Fribourgeois", "Valencay", "Vasterbottenost", "Venaco", "Vendomois",
+ "Vieux Corse", "Vignotte", "Vulscombe", "Waimata Farmhouse Blue",
+ "Washed Rind Cheese (Australian)", "Waterloo", "Weichkaese", "Wellington",
+ "Wensleydale", "White Stilton", "Whitestone Farmhouse", "Wigmore", "Woodside Cabecou",
+ "Xanadu", "Xynotyro", "Yarg Cornish", "Yarra Valley Pyramid", "Yorkshire Blue",
+ "Zamorano", "Zanetti Grana Padano", "Zanetti Parmigiano Reggiano");
diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewHolder.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewHolder.kt
new file mode 100644
index 000000000..a86e99337
--- /dev/null
+++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewHolder.kt
@@ -0,0 +1,42 @@
+/*
+ * 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 paging.android.example.com.pagingsample
+
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.TextView
+
+/**
+ * A simple ViewHolder that can bind a Cheese item. It also accepts null items since the data may
+ * not have been fetched before it is bound.
+ */
+class CheeseViewHolder(parent :ViewGroup) : RecyclerView.ViewHolder(
+ LayoutInflater.from(parent.context).inflate(R.layout.cheese_item, parent, false)) {
+
+ private val nameView = itemView.findViewById(R.id.name)
+ var cheese : Cheese? = null
+
+ /**
+ * Items might be null if they are not paged in yet. PagedListAdapter will re-bind the
+ * ViewHolder when Item is loaded.
+ */
+ fun bindTo(cheese : Cheese?) {
+ this.cheese = cheese
+ nameView.text = cheese?.name
+ }
+}
\ No newline at end of file
diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewModel.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewModel.kt
new file mode 100644
index 000000000..ba63cec2c
--- /dev/null
+++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewModel.kt
@@ -0,0 +1,65 @@
+/*
+ * 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 paging.android.example.com.pagingsample
+
+import android.app.Application
+import android.arch.lifecycle.AndroidViewModel
+import android.arch.paging.LivePagedListBuilder
+import android.arch.paging.PagedList
+
+/**
+ * A simple ViewModel that provides a paged list of delicious Cheeses.
+ */
+class CheeseViewModel(app: Application) : AndroidViewModel(app) {
+ val dao = CheeseDb.get(app).cheeseDao()
+
+ companion object {
+ /**
+ * A good page size is a value that fills at least a screen worth of content on a large
+ * device so the User is unlikely to see a null item.
+ * You can play with this constant to observe the paging behavior.
+ *
+ * It's possible to vary this with list device size, but often unnecessary, unless a user
+ * scrolling on a large device is expected to scroll through items more quickly than a small
+ * device, such as when the large device uses a grid layout of items.
+ */
+ private const val PAGE_SIZE = 30
+
+ /**
+ * If placeholders are enabled, PagedList will report the full size but some items might
+ * be null in onBind method (PagedListAdapter triggers a rebind when data is loaded).
+ *
+ * If placeholders are disabled, onBind will never receive null but as more pages are
+ * loaded, the scrollbars will jitter as new pages are loaded. You should probably disable
+ * scrollbars if you disable placeholders.
+ */
+ private const val ENABLE_PLACEHOLDERS = true
+ }
+
+ val allCheeses = LivePagedListBuilder(dao.allCheesesByName(), PagedList.Config.Builder()
+ .setPageSize(PAGE_SIZE)
+ .setEnablePlaceholders(ENABLE_PLACEHOLDERS)
+ .build()).build()
+
+ fun insert(text: CharSequence) = ioThread {
+ dao.insert(Cheese(id = 0, name = text.toString()))
+ }
+
+ fun remove(cheese: Cheese) = ioThread {
+ dao.delete(cheese)
+ }
+}
\ No newline at end of file
diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/Executors.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/Executors.kt
new file mode 100644
index 000000000..e2d16288f
--- /dev/null
+++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/Executors.kt
@@ -0,0 +1,28 @@
+/*
+ * 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 paging.android.example.com.pagingsample
+
+import java.util.concurrent.Executors
+
+private val IO_EXECUTOR = Executors.newSingleThreadExecutor()
+
+/**
+ * Utility method to run blocks on a dedicated background thread, used for io/database work.
+ */
+fun ioThread(f : () -> Unit) {
+ IO_EXECUTOR.execute(f)
+}
\ No newline at end of file
diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/MainActivity.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/MainActivity.kt
new file mode 100644
index 000000000..abacd377a
--- /dev/null
+++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/MainActivity.kt
@@ -0,0 +1,107 @@
+/*
+ * 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 paging.android.example.com.pagingsample
+
+import android.arch.lifecycle.Observer
+import android.arch.lifecycle.ViewModelProviders
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.widget.RecyclerView
+import android.support.v7.widget.helper.ItemTouchHelper
+import android.view.KeyEvent
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import kotlinx.android.synthetic.main.activity_main.*
+
+/**
+ * Shows a list of Cheeses, with swipe-to-delete, and an input field at the top to add.
+ *
+ * Cheeses are stored in a database, so swipes and additions edit the database directly, and the UI
+ * is updated automatically using paging components.
+ */
+class MainActivity : AppCompatActivity() {
+ private val viewModel by lazy(LazyThreadSafetyMode.NONE) {
+ ViewModelProviders.of(this).get(CheeseViewModel::class.java)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ // Create adapter for the RecyclerView
+ val adapter = CheeseAdapter()
+ cheeseList.adapter = adapter
+
+ // Subscribe the adapter to the ViewModel, so the items in the adapter are refreshed
+ // when the list changes
+ viewModel.allCheeses.observe(this, Observer(adapter::submitList))
+
+ initAddButtonListener()
+ initSwipeToDelete()
+ }
+
+ private fun initSwipeToDelete() {
+ ItemTouchHelper(object : ItemTouchHelper.Callback() {
+ // enable the items to swipe to the left or right
+ override fun getMovementFlags(recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder): Int =
+ makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)
+
+ override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder): Boolean = false
+
+ // When an item is swiped, remove the item via the view model. The list item will be
+ // automatically removed in response, because the adapter is observing the live list.
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder?, direction: Int) {
+ (viewHolder as? CheeseViewHolder)?.cheese?.let {
+ viewModel.remove(it)
+ }
+ }
+ }).attachToRecyclerView(cheeseList)
+ }
+
+ private fun addCheese() {
+ val newCheese = inputText.text.trim()
+ if (newCheese.isNotEmpty()) {
+ viewModel.insert(newCheese)
+ inputText.setText("")
+ }
+ }
+
+ private fun initAddButtonListener() {
+ addButton.setOnClickListener {
+ addCheese()
+ }
+
+ // when the user taps the "Done" button in the on screen keyboard, save the item.
+ inputText.setOnEditorActionListener({ _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ addCheese()
+ return@setOnEditorActionListener true
+ }
+ false // action that isn't DONE occurred - ignore
+ })
+ // When the user clicks on the button, or presses enter, save the item.
+ inputText.setOnKeyListener({ _, keyCode, event ->
+ if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
+ addCheese()
+ return@setOnKeyListener true
+ }
+ false // event that isn't DOWN or ENTER occurred - ignore
+ })
+ }
+}
diff --git a/PagingSample/app/src/main/res/drawable/ic_launcher_background.xml b/PagingSample/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..d8318970f
--- /dev/null
+++ b/PagingSample/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PagingSample/app/src/main/res/layout/activity_main.xml b/PagingSample/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..14f022d48
--- /dev/null
+++ b/PagingSample/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/PagingSample/app/src/main/res/layout/cheese_item.xml b/PagingSample/app/src/main/res/layout/cheese_item.xml
new file mode 100644
index 000000000..646fb051b
--- /dev/null
+++ b/PagingSample/app/src/main/res/layout/cheese_item.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PagingSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/PagingSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..1c67e6ca4
--- /dev/null
+++ b/PagingSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PagingSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/PagingSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..1c67e6ca4
--- /dev/null
+++ b/PagingSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PagingSample/app/src/main/res/mipmap-hdpi/ic_launcher.png b/PagingSample/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..550730310
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/PagingSample/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..4e526c95b
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/PagingSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..8fab6a3a5
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-mdpi/ic_launcher.png b/PagingSample/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..6bc7fcd6f
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/PagingSample/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..2c38c7190
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/PagingSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..1eecc0e7d
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/PagingSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..ec87dcebe
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/PagingSample/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..072467eaa
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/PagingSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..05ca079ca
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/PagingSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..6f67f21ba
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/PagingSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..78a6b7a34
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/PagingSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..8bac0f274
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/PagingSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..0327e13fa
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/PagingSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..68ebe33fe
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/PagingSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/PagingSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..bacd3e758
Binary files /dev/null and b/PagingSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/PagingSample/app/src/main/res/values/colors.xml b/PagingSample/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..86b430488
--- /dev/null
+++ b/PagingSample/app/src/main/res/values/colors.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/PagingSample/app/src/main/res/values/dimens.xml b/PagingSample/app/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..8231557ce
--- /dev/null
+++ b/PagingSample/app/src/main/res/values/dimens.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ 8dp
+
\ No newline at end of file
diff --git a/PagingSample/app/src/main/res/values/strings.xml b/PagingSample/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..7639b13ec
--- /dev/null
+++ b/PagingSample/app/src/main/res/values/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ PagingSample
+ add cheese
+ add
+
diff --git a/PagingSample/app/src/main/res/values/styles.xml b/PagingSample/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..36ef7892b
--- /dev/null
+++ b/PagingSample/app/src/main/res/values/styles.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/PagingSample/build.gradle b/PagingSample/build.gradle
new file mode 100644
index 000000000..5a34e8630
--- /dev/null
+++ b/PagingSample/build.gradle
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ apply from: 'versions.gradle'
+ addRepos(repositories)
+ dependencies {
+ classpath deps.android_gradle_plugin
+ classpath deps.kotlin.plugin
+ }
+}
+
+allprojects {
+ addRepos(repositories)
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/PagingSample/gradle.properties b/PagingSample/gradle.properties
new file mode 100644
index 000000000..be27115b4
--- /dev/null
+++ b/PagingSample/gradle.properties
@@ -0,0 +1,33 @@
+#
+# 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.
+#
+
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/PagingSample/gradle/wrapper/gradle-wrapper.jar b/PagingSample/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..13372aef5
Binary files /dev/null and b/PagingSample/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/PagingSample/gradle/wrapper/gradle-wrapper.properties b/PagingSample/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..f37aa37b0
--- /dev/null
+++ b/PagingSample/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+
+#Tue Sep 12 15:26:15 PDT 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/PagingSample/gradlew b/PagingSample/gradlew
new file mode 100755
index 000000000..9d82f7891
--- /dev/null
+++ b/PagingSample/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/PagingSample/gradlew.bat b/PagingSample/gradlew.bat
new file mode 100644
index 000000000..aec99730b
--- /dev/null
+++ b/PagingSample/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/PagingSample/settings.gradle b/PagingSample/settings.gradle
new file mode 100644
index 000000000..1df87d4d4
--- /dev/null
+++ b/PagingSample/settings.gradle
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ */
+
+include ':app'
diff --git a/PagingSample/versions.gradle b/PagingSample/versions.gradle
new file mode 100644
index 000000000..85822d50b
--- /dev/null
+++ b/PagingSample/versions.gradle
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+
+
+/**
+* Shared file between builds so that they can all use the same dependencies and
+* maven repositories.
+**/
+ext.deps = [:]
+def versions = [:]
+versions.arch_core = "1.1.1"
+versions.room = "1.1.0-beta2"
+versions.lifecycle = "1.1.1"
+versions.support = "27.1.0"
+versions.dagger = "2.11"
+versions.junit = "4.12"
+versions.espresso = "3.0.1"
+versions.retrofit = "2.3.0"
+versions.okhttp_logging_interceptor = "3.9.0"
+versions.mockwebserver = "3.8.1"
+versions.apache_commons = "2.5"
+versions.mockito = "2.7.19"
+versions.mockito_all = "1.10.19"
+versions.dexmaker = "2.2.0"
+versions.constraint_layout = "1.0.2"
+versions.glide = "3.8.0"
+versions.timber = "4.5.1"
+versions.android_gradle_plugin = "3.0.1"
+versions.rxjava2 = "2.1.3"
+versions.rx_android = "2.0.1"
+versions.atsl_runner = "1.0.1"
+versions.atsl_rules = "1.0.1"
+versions.hamcrest = "1.3"
+versions.kotlin = "1.2.20"
+versions.paging = "1.0.0-beta1"
+def deps = [:]
+
+def support = [:]
+support.annotations = "com.android.support:support-annotations:$versions.support"
+support.app_compat = "com.android.support:appcompat-v7:$versions.support"
+support.recyclerview = "com.android.support:recyclerview-v7:$versions.support"
+support.cardview = "com.android.support:cardview-v7:$versions.support"
+support.design = "com.android.support:design:$versions.support"
+support.v4 = "com.android.support:support-v4:$versions.support"
+support.core_utils = "com.android.support:support-core-utils:$versions.support"
+deps.support = support
+
+def room = [:]
+room.runtime = "android.arch.persistence.room:runtime:$versions.room"
+room.compiler = "android.arch.persistence.room:compiler:$versions.room"
+room.rxjava2 = "android.arch.persistence.room:rxjava2:$versions.room"
+room.testing = "android.arch.persistence.room:testing:$versions.room"
+deps.room = room
+
+def lifecycle = [:]
+lifecycle.runtime = "android.arch.lifecycle:runtime:$versions.lifecycle"
+lifecycle.extensions = "android.arch.lifecycle:extensions:$versions.lifecycle"
+lifecycle.java8 = "android.arch.lifecycle:common-java8:$versions.lifecycle"
+lifecycle.compiler = "android.arch.lifecycle:compiler:$versions.lifecycle"
+deps.lifecycle = lifecycle
+
+def arch_core = [:]
+arch_core.testing = "android.arch.core:core-testing:$versions.arch_core"
+deps.arch_core = arch_core
+
+def retrofit = [:]
+retrofit.runtime = "com.squareup.retrofit2:retrofit:$versions.retrofit"
+retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit"
+retrofit.mock = "com.squareup.retrofit2:retrofit-mock:$versions.retrofit"
+deps.retrofit = retrofit
+deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:${versions.okhttp_logging_interceptor}"
+
+def dagger = [:]
+dagger.runtime = "com.google.dagger:dagger:$versions.dagger"
+dagger.android = "com.google.dagger:dagger-android:$versions.dagger"
+dagger.android_support = "com.google.dagger:dagger-android-support:$versions.dagger"
+dagger.compiler = "com.google.dagger:dagger-compiler:$versions.dagger"
+dagger.android_support_compiler = "com.google.dagger:dagger-android-processor:$versions.dagger"
+
+deps.dagger = dagger
+
+def espresso = [:]
+espresso.core = "com.android.support.test.espresso:espresso-core:$versions.espresso"
+espresso.contrib = "com.android.support.test.espresso:espresso-contrib:$versions.espresso"
+espresso.intents = "com.android.support.test.espresso:espresso-intents:$versions.espresso"
+deps.espresso = espresso
+
+def atsl = [:]
+atsl.runner = "com.android.support.test:runner:$versions.atsl_runner"
+atsl.rules = "com.android.support.test:rules:$versions.atsl_runner"
+deps.atsl = atsl
+
+def mockito = [:]
+mockito.core = "org.mockito:mockito-core:$versions.mockito"
+mockito.all = "org.mockito:mockito-all:$versions.mockito_all"
+deps.mockito = mockito
+
+def kotlin = [:]
+kotlin.stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jre7:$versions.kotlin"
+kotlin.test = "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin"
+kotlin.plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
+
+deps.kotlin = kotlin
+deps.paging = "android.arch.paging:runtime:$versions.paging"
+deps.glide = "com.github.bumptech.glide:glide:$versions.glide"
+deps.dexmaker = "com.linkedin.dexmaker:dexmaker-mockito:$versions.dexmaker"
+deps.constraint_layout = "com.android.support.constraint:constraint-layout:$versions.constraint_layout"
+deps.timber = "com.jakewharton.timber:timber:$versions.timber"
+deps.junit = "junit:junit:$versions.junit"
+deps.mock_web_server = "com.squareup.okhttp3:mockwebserver:$versions.mockwebserver"
+deps.rxjava2 = "io.reactivex.rxjava2:rxjava:$versions.rxjava2"
+deps.rx_android = "io.reactivex.rxjava2:rxandroid:$versions.rx_android"
+deps.hamcrest = "org.hamcrest:hamcrest-all:$versions.hamcrest"
+deps.android_gradle_plugin = "com.android.tools.build:gradle:$versions.android_gradle_plugin"
+ext.deps = deps
+
+def build_versions = [:]
+build_versions.min_sdk = 14
+build_versions.target_sdk = 26
+build_versions.build_tools = "27.0.3"
+ext.build_versions = build_versions
+
+
+def addRepos(RepositoryHandler handler) {
+ handler.google()
+ handler.jcenter()
+ handler.maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
+}
+ext.addRepos = this.&addRepos
diff --git a/PagingWithNetworkSample/.gitignore b/PagingWithNetworkSample/.gitignore
new file mode 100644
index 000000000..39fb081a4
--- /dev/null
+++ b/PagingWithNetworkSample/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/PagingWithNetworkSample/README.md b/PagingWithNetworkSample/README.md
new file mode 100644
index 000000000..6e5159c18
--- /dev/null
+++ b/PagingWithNetworkSample/README.md
@@ -0,0 +1,69 @@
+# Paging With Network Sample
+
+This sample demonstrates how to use the Paging library with a backend API (in this
+case [Reddit API][8]).
+
+There are 3 variations of the demo, which you can select in the `MainActivity` class.
+
+After selecting an option, it starts the `RedditActivity` which is the activity that
+shows the list of posts in a given subreddit.
+
+## Paging With Database And Network
+This sample, implemented in the [DbRedditPostRepository][1] class, demonstrates how to set up
+a Repository that will use the local database to page in data for the UI and also back-fill
+the database from the network as the user reaches to the end of the data in the database.
+
+It uses `Room` to create the `DataSource.Factory` ([dao][3]) and the Paging Boundary Callback
+API to get notified when the Paging library consumes the available local data ([boundary callback
+implementation][4])
+
+This usually provides the best user experience as the cached content is always available
+on the device and the user will still have a good experience even if the network is slow /
+unavailable.
+
+## Paging Using Item Keys
+This sample, implemented in the [InMemoryByItemRepository][2] class, demonstrates how to
+set up a Repository that will directly page in from the network and will use the `key` from
+the previous item to find the request parameters for the next page.
+
+[ItemKeyedSubredditDataSource][5]: The data source that uses the `key` in items
+(`name` in Reddit API) to find the next page. It extends from the `ItemKeyedDataSource` class
+in the Paging Library.
+
+## Paging Using Next Tokens From The Previous Query
+This sample, implemented in the [InMemoryByPageKeyRepository][6] class, demonstrates how to
+utilize the `before` and `after` keys in the response to discover the next page. (This is
+the intended use of the Reddit API but this sample still provides
+[ItemKeyedSubredditDataSource][5] to serve as an example if the backend does not provide
+before/after links)
+
+[PageKeyedSubredditDataSource][7]: The data source that uses the `after` and `before` fields
+in the API request response. It extends from the `PageKeyedDataSource` class
+in the Paging Library.
+
+
+### Libraries
+* [Android Support Library][support-lib]
+* [Android Architecture Components][arch]
+* [Retrofit][retrofit] for REST api communication
+* [Glide][glide] for image loading
+* [espresso][espresso] for UI tests
+* [mockito][mockito] for mocking in tests
+* [Retrofit Mock][retrofit-mock] for creating a fake API implementation for tests
+
+[1]: app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inDb/DbRedditPostRepository.kt
+[2]: app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/InMemoryByItemRepository.kt
+[3]: app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/db/RedditPostDao.kt
+[4]: app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inDb/SubredditBoundaryCallback.kt
+[5]: app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/ItemKeyedSubredditDataSource.kt
+[6]: app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/InMemoryByPageKeyRepository.kt
+[7]: app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/PageKeyedSubredditDataSource.kt
+[8]: https://www.reddit.com/dev/api/#listings
+[mockwebserver]: https://github.com/square/okhttp/tree/master/mockwebserver
+[support-lib]: https://developer.android.com/topic/libraries/support-library/index.html
+[arch]: https://developer.android.com/arch
+[espresso]: https://google.github.io/android-testing-support-library/docs/espresso/
+[retrofit]: http://square.github.io/retrofit
+[glide]: https://github.com/bumptech/glide
+[mockito]: http://site.mockito.org
+[retrofit-mock]: https://github.com/square/retrofit/tree/master/retrofit-mock
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/.gitignore b/PagingWithNetworkSample/app/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/PagingWithNetworkSample/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/PagingWithNetworkSample/app/build.gradle b/PagingWithNetworkSample/app/build.gradle
new file mode 100644
index 000000000..65c8f2e7c
--- /dev/null
+++ b/PagingWithNetworkSample/app/build.gradle
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+apply plugin: 'com.android.application'
+
+apply plugin: 'kotlin-android'
+
+apply plugin: 'kotlin-android-extensions'
+
+apply plugin: 'kotlin-kapt'
+
+android {
+ compileSdkVersion build_versions.target_sdk
+ buildToolsVersion build_versions.build_tools
+ defaultConfig {
+ applicationId "com.android.example.paging.pagingwithnetwork"
+ minSdkVersion build_versions.min_sdk
+ targetSdkVersion build_versions.target_sdk
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ sourceSets {
+ androidTest.java.srcDirs += "src/test-common/java"
+ test.java.srcDirs += "src/test-common/java"
+ }
+}
+
+
+dependencies {
+ kapt deps.room.compiler
+
+ implementation deps.constraint_layout
+ implementation deps.support.app_compat
+ implementation deps.support.recyclerview
+ implementation deps.support.cardview
+ implementation deps.support.design
+ implementation deps.room.runtime
+ implementation deps.lifecycle.runtime
+ implementation deps.lifecycle.extensions
+ implementation deps.paging
+ implementation deps.kotlin.stdlib
+ implementation deps.retrofit.runtime
+ implementation deps.retrofit.gson
+ implementation deps.okhttp_logging_interceptor
+ implementation deps.glide
+
+ // Android Testing Support Library's runner and rules
+ androidTestImplementation deps.atsl.runner
+ androidTestImplementation deps.atsl.rules
+ androidTestImplementation deps.arch_core.testing
+
+ androidTestImplementation deps.junit
+ androidTestImplementation deps.retrofit.mock
+ androidTestImplementation deps.arch_core.testing
+ androidTestImplementation deps.dexmaker
+ androidTestImplementation deps.mockito.core, { exclude group: 'net.bytebuddy' }
+
+ testImplementation deps.junit
+ testImplementation deps.retrofit.mock
+ testImplementation deps.arch_core.testing
+ testImplementation deps.mockito.core
+}
diff --git a/PagingWithNetworkSample/app/proguard-rules.pro b/PagingWithNetworkSample/app/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/PagingWithNetworkSample/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/PagingWithNetworkSample/app/src/androidTest/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivityTest.kt b/PagingWithNetworkSample/app/src/androidTest/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivityTest.kt
new file mode 100644
index 000000000..842735b81
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/androidTest/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivityTest.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.ui
+
+import android.app.Application
+import android.content.Intent
+import android.support.test.InstrumentationRegistry
+import android.support.v7.widget.RecyclerView
+import com.android.example.paging.pagingwithnetwork.R
+import com.android.example.paging.pagingwithnetwork.reddit.DefaultServiceLocator
+import com.android.example.paging.pagingwithnetwork.reddit.ServiceLocator
+import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi
+import com.android.example.paging.pagingwithnetwork.reddit.repository.RedditPostRepository
+import com.android.example.paging.pagingwithnetwork.reddit.ui.RedditActivity.Companion.DEFAULT_SUBREDDIT
+import com.android.example.paging.pagingwithnetwork.repository.FakeRedditApi
+import com.android.example.paging.pagingwithnetwork.repository.PostFactory
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.notNullValue
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+/**
+ * Simple sanity test to ensure data is displayed
+ */
+@RunWith(Parameterized::class)
+class RedditActivityTest(private val type: RedditPostRepository.Type) {
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = RedditPostRepository.Type.values()
+ }
+
+ private val postFactory = PostFactory()
+ @Before
+ fun init() {
+ val fakeApi = FakeRedditApi()
+ fakeApi.addPost(postFactory.createRedditPost(DEFAULT_SUBREDDIT))
+ fakeApi.addPost(postFactory.createRedditPost(DEFAULT_SUBREDDIT))
+ fakeApi.addPost(postFactory.createRedditPost(DEFAULT_SUBREDDIT))
+ val app = InstrumentationRegistry.getTargetContext().applicationContext as Application
+ // use a controlled service locator w/ fake API
+ ServiceLocator.swap(
+ object : DefaultServiceLocator(app = app,
+ useInMemoryDb = true) {
+ override fun getRedditApi(): RedditApi = fakeApi
+ }
+ )
+ }
+
+ @Test
+ @Throws(InterruptedException::class, TimeoutException::class)
+ fun showSomeResults() {
+ val intent = RedditActivity.intentFor(
+ context = InstrumentationRegistry.getTargetContext(),
+ type = type)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ val activity = InstrumentationRegistry.getInstrumentation().startActivitySync(intent)
+ val recyclerView = activity.findViewById(R.id.list)
+ assertThat(recyclerView.adapter, notNullValue())
+ waitForAdapterChange(recyclerView)
+ assertThat(recyclerView.adapter.itemCount, `is`(3))
+ }
+
+ private fun waitForAdapterChange(recyclerView: RecyclerView) {
+ val latch = CountDownLatch(1)
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ recyclerView.adapter.registerAdapterDataObserver(
+ object : RecyclerView.AdapterDataObserver() {
+ override fun onChanged() {
+ latch.countDown()
+ }
+
+ override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
+ latch.countDown()
+ }
+ })
+ }
+ if (recyclerView.adapter.itemCount > 0) {
+ return
+ }
+ assertThat(latch.await(10, TimeUnit.SECONDS), `is`(true))
+ }
+}
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/src/main/AndroidManifest.xml b/PagingWithNetworkSample/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..b637cf977
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/AndroidManifest.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/src/main/java/android/arch/paging/PagingRequestHelper.java b/PagingWithNetworkSample/app/src/main/java/android/arch/paging/PagingRequestHelper.java
new file mode 100644
index 000000000..941f1173a
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/android/arch/paging/PagingRequestHelper.java
@@ -0,0 +1,490 @@
+/*
+ * 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 android.arch.paging;
+import android.support.annotation.AnyThread;
+import android.support.annotation.GuardedBy;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import java.util.Arrays;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+/**
+ * A helper class for {@link android.arch.paging.PagedList.BoundaryCallback BoundaryCallback}s and
+ * {@link DataSource}s to help with tracking network requests.
+ *
+ * It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL},
+ * {@link RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request
+ * for each of them via {@link #runIfNotRunning(RequestType, Request)}.
+ *
+ * It tracks a {@link Status} and an {@code error} for each {@link RequestType}.
+ *
+ * A sample usage of this class to limit requests looks like this:
+ *
+ * class PagingBoundaryCallback extends PagedList.BoundaryCallback<MyItem> {
+ * // TODO replace with an executor from your application
+ * Executor executor = Executors.newSingleThreadExecutor();
+ * PagingRequestHelper helper = new PagingRequestHelper(executor);
+ * // imaginary API service, using Retrofit
+ * MyApi api;
+ *
+ * {@literal @}Override
+ * public void onItemAtFrontLoaded({@literal @}NonNull MyItem itemAtFront) {
+ * helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE,
+ * helperCallback -> api.getTopBefore(itemAtFront.getName(), 10).enqueue(
+ * new Callback<ApiResponse>() {
+ * {@literal @}Override
+ * public void onResponse(Call<ApiResponse> call,
+ * Response<ApiResponse> response) {
+ * // TODO insert new records into database
+ * helperCallback.recordSuccess();
+ * }
+ *
+ * {@literal @}Override
+ * public void onFailure(Call<ApiResponse> call, Throwable t) {
+ * helperCallback.recordFailure(t);
+ * }
+ * }));
+ * }
+ *
+ * {@literal @}Override
+ * public void onItemAtEndLoaded({@literal @}NonNull MyItem itemAtEnd) {
+ * helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER,
+ * helperCallback -> api.getTopBefore(itemAtEnd.getName(), 10).enqueue(
+ * new Callback<ApiResponse>() {
+ * {@literal @}Override
+ * public void onResponse(Call<ApiResponse> call,
+ * Response<ApiResponse> response) {
+ * // TODO insert new records into database
+ * helperCallback.recordSuccess();
+ * }
+ *
+ * {@literal @}Override
+ * public void onFailure(Call<ApiResponse> call, Throwable t) {
+ * helperCallback.recordFailure(t);
+ * }
+ * }));
+ * }
+ * }
+ *
+ *
+ * The helper provides an API to observe combined request status, which can be reported back to the
+ * application based on your business rules.
+ *
+ * MutableLiveData<PagingRequestHelper.Status> combined = new MutableLiveData<>();
+ * helper.addListener(status -> {
+ * // merge multiple states per request type into one, or dispatch separately depending on
+ * // your application logic.
+ * if (status.hasRunning()) {
+ * combined.postValue(PagingRequestHelper.Status.RUNNING);
+ * } else if (status.hasError()) {
+ * // can also obtain the error via {@link StatusReport#getErrorFor(RequestType)}
+ * combined.postValue(PagingRequestHelper.Status.FAILED);
+ * } else {
+ * combined.postValue(PagingRequestHelper.Status.SUCCESS);
+ * }
+ * });
+ *
+ */
+// THIS class is likely to be moved into the library in a future release. Feel free to copy it
+// from this sample.
+public class PagingRequestHelper {
+ private final Object mLock = new Object();
+ private final Executor mRetryService;
+ @GuardedBy("mLock")
+ private final RequestQueue[] mRequestQueues = new RequestQueue[]
+ {new RequestQueue(RequestType.INITIAL),
+ new RequestQueue(RequestType.BEFORE),
+ new RequestQueue(RequestType.AFTER)};
+ @NonNull
+ final CopyOnWriteArrayList mListeners = new CopyOnWriteArrayList<>();
+ /**
+ * Creates a new PagingRequestHelper with the given {@link Executor} which is used to run
+ * retry actions.
+ *
+ * @param retryService The {@link Executor} that can run the retry actions.
+ */
+ public PagingRequestHelper(@NonNull Executor retryService) {
+ mRetryService = retryService;
+ }
+ /**
+ * Adds a new listener that will be notified when any request changes {@link Status state}.
+ *
+ * @param listener The listener that will be notified each time a request's status changes.
+ * @return True if it is added, false otherwise (e.g. it already exists in the list).
+ */
+ @AnyThread
+ public boolean addListener(@NonNull Listener listener) {
+ return mListeners.add(listener);
+ }
+ /**
+ * Removes the given listener from the listeners list.
+ *
+ * @param listener The listener that will be removed.
+ * @return True if the listener is removed, false otherwise (e.g. it never existed)
+ */
+ public boolean removeListener(@NonNull Listener listener) {
+ return mListeners.remove(listener);
+ }
+ /**
+ * Runs the given {@link Request} if no other requests in the given request type is already
+ * running.
+ *
+ * If run, the request will be run in the current thread.
+ *
+ * @param type The type of the request.
+ * @param request The request to run.
+ * @return True if the request is run, false otherwise.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @AnyThread
+ public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) {
+ boolean hasListeners = !mListeners.isEmpty();
+ StatusReport report = null;
+ synchronized (mLock) {
+ RequestQueue queue = mRequestQueues[type.ordinal()];
+ if (queue.mRunning != null) {
+ return false;
+ }
+ queue.mRunning = request;
+ queue.mStatus = Status.RUNNING;
+ queue.mFailed = null;
+ queue.mLastError = null;
+ if (hasListeners) {
+ report = prepareStatusReportLocked();
+ }
+ }
+ if (report != null) {
+ dispatchReport(report);
+ }
+ final RequestWrapper wrapper = new RequestWrapper(request, this, type);
+ wrapper.run();
+ return true;
+ }
+ @GuardedBy("mLock")
+ private StatusReport prepareStatusReportLocked() {
+ Throwable[] errors = new Throwable[]{
+ mRequestQueues[0].mLastError,
+ mRequestQueues[1].mLastError,
+ mRequestQueues[2].mLastError
+ };
+ return new StatusReport(
+ getStatusForLocked(RequestType.INITIAL),
+ getStatusForLocked(RequestType.BEFORE),
+ getStatusForLocked(RequestType.AFTER),
+ errors
+ );
+ }
+ @GuardedBy("mLock")
+ private Status getStatusForLocked(RequestType type) {
+ return mRequestQueues[type.ordinal()].mStatus;
+ }
+ @AnyThread
+ @VisibleForTesting
+ void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) {
+ StatusReport report = null;
+ final boolean success = throwable == null;
+ boolean hasListeners = !mListeners.isEmpty();
+ synchronized (mLock) {
+ RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()];
+ queue.mRunning = null;
+ queue.mLastError = throwable;
+ if (success) {
+ queue.mFailed = null;
+ queue.mStatus = Status.SUCCESS;
+ } else {
+ queue.mFailed = wrapper;
+ queue.mStatus = Status.FAILED;
+ }
+ if (hasListeners) {
+ report = prepareStatusReportLocked();
+ }
+ }
+ if (report != null) {
+ dispatchReport(report);
+ }
+ }
+ private void dispatchReport(StatusReport report) {
+ for (Listener listener : mListeners) {
+ listener.onStatusChange(report);
+ }
+ }
+ /**
+ * Retries all failed requests.
+ *
+ * @return True if any request is retried, false otherwise.
+ */
+ public boolean retryAllFailed() {
+ final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length];
+ boolean retried = false;
+ synchronized (mLock) {
+ for (int i = 0; i < RequestType.values().length; i++) {
+ toBeRetried[i] = mRequestQueues[i].mFailed;
+ mRequestQueues[i].mFailed = null;
+ }
+ }
+ for (RequestWrapper failed : toBeRetried) {
+ if (failed != null) {
+ failed.retry(mRetryService);
+ retried = true;
+ }
+ }
+ return retried;
+ }
+ static class RequestWrapper implements Runnable {
+ @NonNull
+ final Request mRequest;
+ @NonNull
+ final PagingRequestHelper mHelper;
+ @NonNull
+ final RequestType mType;
+ RequestWrapper(@NonNull Request request, @NonNull PagingRequestHelper helper,
+ @NonNull RequestType type) {
+ mRequest = request;
+ mHelper = helper;
+ mType = type;
+ }
+ @Override
+ public void run() {
+ mRequest.run(new Request.Callback(this, mHelper));
+ }
+ void retry(Executor service) {
+ service.execute(new Runnable() {
+ @Override
+ public void run() {
+ mHelper.runIfNotRunning(mType, mRequest);
+ }
+ });
+ }
+ }
+ /**
+ * Runner class that runs a request tracked by the {@link PagingRequestHelper}.
+ *
+ * When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)}
+ * or {@link Callback#recordSuccess()} once and only once. This call
+ * can be made any time. Until that method call is made, {@link PagingRequestHelper} will
+ * consider the request is running.
+ */
+ @FunctionalInterface
+ public interface Request {
+ /**
+ * Should run the request and call the given {@link Callback} with the result of the
+ * request.
+ *
+ * @param callback The callback that should be invoked with the result.
+ */
+ void run(Callback callback);
+ /**
+ * Callback class provided to the {@link #run(Callback)} method to report the result.
+ */
+ class Callback {
+ private final AtomicBoolean mCalled = new AtomicBoolean();
+ private final RequestWrapper mWrapper;
+ private final PagingRequestHelper mHelper;
+ Callback(RequestWrapper wrapper, PagingRequestHelper helper) {
+ mWrapper = wrapper;
+ mHelper = helper;
+ }
+ /**
+ * Call this method when the request succeeds and new data is fetched.
+ */
+ @SuppressWarnings("unused")
+ public final void recordSuccess() {
+ if (mCalled.compareAndSet(false, true)) {
+ mHelper.recordResult(mWrapper, null);
+ } else {
+ throw new IllegalStateException(
+ "already called recordSuccess or recordFailure");
+ }
+ }
+ /**
+ * Call this method with the failure message and the request can be retried via
+ * {@link #retryAllFailed()}.
+ *
+ * @param throwable The error that occured while carrying out the request.
+ */
+ @SuppressWarnings("unused")
+ public final void recordFailure(@NonNull Throwable throwable) {
+ //noinspection ConstantConditions
+ if (throwable == null) {
+ throw new IllegalArgumentException("You must provide a throwable describing"
+ + " the error to record the failure");
+ }
+ if (mCalled.compareAndSet(false, true)) {
+ mHelper.recordResult(mWrapper, throwable);
+ } else {
+ throw new IllegalStateException(
+ "already called recordSuccess or recordFailure");
+ }
+ }
+ }
+ }
+ /**
+ * Data class that holds the information about the current status of the ongoing requests
+ * using this helper.
+ */
+ public static final class StatusReport {
+ /**
+ * Status of the latest request that were submitted with {@link RequestType#INITIAL}.
+ */
+ @NonNull
+ public final Status initial;
+ /**
+ * Status of the latest request that were submitted with {@link RequestType#BEFORE}.
+ */
+ @NonNull
+ public final Status before;
+ /**
+ * Status of the latest request that were submitted with {@link RequestType#AFTER}.
+ */
+ @NonNull
+ public final Status after;
+ @NonNull
+ private final Throwable[] mErrors;
+ StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after,
+ @NonNull Throwable[] errors) {
+ this.initial = initial;
+ this.before = before;
+ this.after = after;
+ this.mErrors = errors;
+ }
+ /**
+ * Convenience method to check if there are any running requests.
+ *
+ * @return True if there are any running requests, false otherwise.
+ */
+ public boolean hasRunning() {
+ return initial == Status.RUNNING
+ || before == Status.RUNNING
+ || after == Status.RUNNING;
+ }
+ /**
+ * Convenience method to check if there are any requests that resulted in an error.
+ *
+ * @return True if there are any requests that finished with error, false otherwise.
+ */
+ public boolean hasError() {
+ return initial == Status.FAILED
+ || before == Status.FAILED
+ || after == Status.FAILED;
+ }
+ /**
+ * Returns the error for the given request type.
+ *
+ * @param type The request type for which the error should be returned.
+ * @return The {@link Throwable} returned by the failing request with the given type or
+ * {@code null} if the request for the given type did not fail.
+ */
+ @Nullable
+ public Throwable getErrorFor(@NonNull RequestType type) {
+ return mErrors[type.ordinal()];
+ }
+ @Override
+ public String toString() {
+ return "StatusReport{"
+ + "initial=" + initial
+ + ", before=" + before
+ + ", after=" + after
+ + ", mErrors=" + Arrays.toString(mErrors)
+ + '}';
+ }
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ StatusReport that = (StatusReport) o;
+ if (initial != that.initial) return false;
+ if (before != that.before) return false;
+ if (after != that.after) return false;
+ // Probably incorrect - comparing Object[] arrays with Arrays.equals
+ return Arrays.equals(mErrors, that.mErrors);
+ }
+ @Override
+ public int hashCode() {
+ int result = initial.hashCode();
+ result = 31 * result + before.hashCode();
+ result = 31 * result + after.hashCode();
+ result = 31 * result + Arrays.hashCode(mErrors);
+ return result;
+ }
+ }
+ /**
+ * Listener interface to get notified by request status changes.
+ */
+ public interface Listener {
+ /**
+ * Called when the status for any of the requests has changed.
+ *
+ * @param report The current status report that has all the information about the requests.
+ */
+ void onStatusChange(@NonNull StatusReport report);
+ }
+ /**
+ * Represents the status of a Request for each {@link RequestType}.
+ */
+ public enum Status {
+ /**
+ * There is current a running request.
+ */
+ RUNNING,
+ /**
+ * The last request has succeeded or no such requests have ever been run.
+ */
+ SUCCESS,
+ /**
+ * The last request has failed.
+ */
+ FAILED
+ }
+ /**
+ * Available request types.
+ */
+ public enum RequestType {
+ /**
+ * Corresponds to an initial request made to a {@link DataSource} or the empty state for
+ * a {@link android.arch.paging.PagedList.BoundaryCallback BoundaryCallback}.
+ */
+ INITIAL,
+ /**
+ * Corresponds to the {@code loadBefore} calls in {@link DataSource} or
+ * {@code onItemAtFrontLoaded} in
+ * {@link android.arch.paging.PagedList.BoundaryCallback BoundaryCallback}.
+ */
+ BEFORE,
+ /**
+ * Corresponds to the {@code loadAfter} calls in {@link DataSource} or
+ * {@code onItemAtEndLoaded} in
+ * {@link android.arch.paging.PagedList.BoundaryCallback BoundaryCallback}.
+ */
+ AFTER
+ }
+ class RequestQueue {
+ @NonNull
+ final RequestType mRequestType;
+ @Nullable
+ RequestWrapper mFailed;
+ @Nullable
+ Request mRunning;
+ @Nullable
+ Throwable mLastError;
+ @NonNull
+ Status mStatus = Status.SUCCESS;
+ RequestQueue(@NonNull RequestType requestType) {
+ mRequestType = requestType;
+ }
+ }
+}
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/MainActivity.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/MainActivity.kt
new file mode 100644
index 000000000..f86b3a8a7
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/MainActivity.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork
+
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+import com.android.example.paging.pagingwithnetwork.reddit.repository.RedditPostRepository
+import com.android.example.paging.pagingwithnetwork.reddit.ui.RedditActivity
+import kotlinx.android.synthetic.main.activity_main.*
+
+/**
+ * chooser activity for the demo.
+ */
+class MainActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ withDatabase.setOnClickListener {
+ show(RedditPostRepository.Type.DB)
+ }
+ networkOnly.setOnClickListener {
+ show(RedditPostRepository.Type.IN_MEMORY_BY_ITEM)
+ }
+ networkOnlyWithPageKeys.setOnClickListener {
+ show(RedditPostRepository.Type.IN_MEMORY_BY_PAGE)
+ }
+ }
+
+ private fun show(type: RedditPostRepository.Type) {
+ val intent = RedditActivity.intentFor(this, type)
+ startActivity(intent)
+ }
+}
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ServiceLocator.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ServiceLocator.kt
new file mode 100644
index 000000000..b0ce7af67
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ServiceLocator.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit
+
+import android.app.Application
+import android.content.Context
+import android.support.annotation.VisibleForTesting
+import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi
+import com.android.example.paging.pagingwithnetwork.reddit.db.RedditDb
+import com.android.example.paging.pagingwithnetwork.reddit.repository.RedditPostRepository
+import com.android.example.paging.pagingwithnetwork.reddit.repository.inDb.DbRedditPostRepository
+import com.android.example.paging.pagingwithnetwork.reddit.repository.inMemory.byItem.InMemoryByItemRepository
+import com.android.example.paging.pagingwithnetwork.reddit.repository.inMemory.byPage.InMemoryByPageKeyRepository
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+
+/**
+ * Super simplified service locator implementation to allow us to replace default implementations
+ * for testing.
+ */
+interface ServiceLocator {
+ companion object {
+ private val LOCK = Any()
+ private var instance: ServiceLocator? = null
+ fun instance(context: Context): ServiceLocator {
+ synchronized(LOCK) {
+ if (instance == null) {
+ instance = DefaultServiceLocator(
+ app = context.applicationContext as Application,
+ useInMemoryDb = false)
+ }
+ return instance!!
+ }
+ }
+
+ /**
+ * Allows tests to replace the default implementations.
+ */
+ @VisibleForTesting
+ fun swap(locator: ServiceLocator) {
+ instance = locator
+ }
+ }
+
+ fun getRepository(type: RedditPostRepository.Type): RedditPostRepository
+
+ fun getNetworkExecutor(): Executor
+
+ fun getDiskIOExecutor(): Executor
+
+ fun getRedditApi(): RedditApi
+}
+
+/**
+ * default implementation of ServiceLocator that uses production endpoints.
+ */
+open class DefaultServiceLocator(val app: Application, val useInMemoryDb: Boolean) : ServiceLocator {
+ // thread pool used for disk access
+ @Suppress("PrivatePropertyName")
+ private val DISK_IO = Executors.newSingleThreadExecutor()
+
+ // thread pool used for network requests
+ @Suppress("PrivatePropertyName")
+ private val NETWORK_IO = Executors.newFixedThreadPool(5)
+
+ private val db by lazy {
+ RedditDb.create(app, useInMemoryDb)
+ }
+
+ private val api by lazy {
+ RedditApi.create()
+ }
+
+ override fun getRepository(type: RedditPostRepository.Type): RedditPostRepository {
+ return when (type) {
+ RedditPostRepository.Type.IN_MEMORY_BY_ITEM -> InMemoryByItemRepository(
+ redditApi = getRedditApi(),
+ networkExecutor = getNetworkExecutor())
+ RedditPostRepository.Type.IN_MEMORY_BY_PAGE -> InMemoryByPageKeyRepository(
+ redditApi = getRedditApi(),
+ networkExecutor = getNetworkExecutor())
+ RedditPostRepository.Type.DB -> DbRedditPostRepository(
+ db = db,
+ redditApi = getRedditApi(),
+ ioExecutor = getDiskIOExecutor())
+ }
+ }
+
+ override fun getNetworkExecutor(): Executor = NETWORK_IO
+
+ override fun getDiskIOExecutor(): Executor = DISK_IO
+
+ override fun getRedditApi(): RedditApi = api
+}
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/api/RedditApi.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/api/RedditApi.kt
new file mode 100644
index 000000000..80a2a9357
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/api/RedditApi.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.api
+
+import android.util.Log
+import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost
+import okhttp3.HttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Call
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import retrofit2.http.GET
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+/**
+ * API communication setup
+ */
+interface RedditApi {
+ @GET("/r/{subreddit}/hot.json")
+ fun getTop(
+ @Path("subreddit") subreddit: String,
+ @Query("limit") limit: Int): Call
+
+ // for after/before param, either get from RedditDataResponse.after/before,
+ // or pass RedditNewsDataResponse.name (though this is technically incorrect)
+ @GET("/r/{subreddit}/hot.json")
+ fun getTopAfter(
+ @Path("subreddit") subreddit: String,
+ @Query("after") after: String,
+ @Query("limit") limit: Int): Call
+
+ @GET("/r/{subreddit}/hot.json")
+ fun getTopBefore(
+ @Path("subreddit") subreddit: String,
+ @Query("before") before: String,
+ @Query("limit") limit: Int): Call
+
+ class ListingResponse(val data: ListingData)
+
+ class ListingData(
+ val children: List,
+ val after: String?,
+ val before: String?
+ )
+
+ data class RedditChildrenResponse(val data: RedditPost)
+
+ companion object {
+ private const val BASE_URL = "https://www.reddit.com/"
+ fun create(): RedditApi = create(HttpUrl.parse(BASE_URL)!!)
+ fun create(httpUrl: HttpUrl): RedditApi {
+ val logger = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger {
+ Log.d("API", it)
+ })
+ logger.level = HttpLoggingInterceptor.Level.BASIC
+
+ val client = OkHttpClient.Builder()
+ .addInterceptor(logger)
+ .build()
+ return Retrofit.Builder()
+ .baseUrl(httpUrl)
+ .client(client)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ .create(RedditApi::class.java)
+ }
+ }
+}
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/db/RedditDb.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/db/RedditDb.kt
new file mode 100644
index 000000000..d9032eec5
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/db/RedditDb.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.db
+
+import android.arch.persistence.room.Database
+import android.arch.persistence.room.Room
+import android.arch.persistence.room.RoomDatabase
+import android.content.Context
+import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost
+
+/**
+ * Database schema used by the DbRedditPostRepository
+ */
+@Database(
+ entities = arrayOf(RedditPost::class),
+ version = 1,
+ exportSchema = false
+)
+abstract class RedditDb : RoomDatabase() {
+ companion object {
+ fun create(context: Context, useInMemory : Boolean): RedditDb {
+ val databaseBuilder = if(useInMemory) {
+ Room.inMemoryDatabaseBuilder(context, RedditDb::class.java)
+ } else {
+ Room.databaseBuilder(context, RedditDb::class.java, "reddit.db")
+ }
+ return databaseBuilder
+ .fallbackToDestructiveMigration()
+ .build()
+ }
+ }
+
+ abstract fun posts(): RedditPostDao
+}
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/db/RedditPostDao.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/db/RedditPostDao.kt
new file mode 100644
index 000000000..a82e468ec
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/db/RedditPostDao.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.db
+
+import android.arch.paging.DataSource
+import android.arch.persistence.room.Dao
+import android.arch.persistence.room.Insert
+import android.arch.persistence.room.OnConflictStrategy
+import android.arch.persistence.room.Query
+import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost
+
+@Dao
+interface RedditPostDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(posts : List)
+
+ @Query("SELECT * FROM posts WHERE subreddit = :subreddit ORDER BY indexInResponse ASC")
+ fun postsBySubreddit(subreddit : String) : DataSource.Factory
+
+ @Query("DELETE FROM posts WHERE subreddit = :subreddit")
+ fun deleteBySubreddit(subreddit: String)
+
+ @Query("SELECT MAX(indexInResponse) + 1 FROM posts WHERE subreddit = :subreddit")
+ fun getNextIndexInSubreddit(subreddit: String) : Int
+}
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/Listing.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/Listing.kt
new file mode 100644
index 000000000..44d3637fe
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/Listing.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.repository
+
+import android.arch.lifecycle.LiveData
+import android.arch.paging.PagedList
+
+/**
+ * Data class that is necessary for a UI to show a listing and interact w/ the rest of the system
+ */
+data class Listing(
+ // the LiveData of paged lists for the UI to observe
+ val pagedList: LiveData>,
+ // represents the network request status to show to the user
+ val networkState: LiveData,
+ // represents the refresh status to show to the user. Separate from networkState, this
+ // value is importantly only when refresh is requested.
+ val refreshState: LiveData,
+ // refreshes the whole data and fetches it from scratch.
+ val refresh: () -> Unit,
+ // retries any failed requests.
+ val retry: () -> Unit)
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/NetworkState.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/NetworkState.kt
new file mode 100644
index 000000000..0141be4eb
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/NetworkState.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.repository
+
+enum class Status {
+ RUNNING,
+ SUCCESS,
+ FAILED
+}
+
+@Suppress("DataClassPrivateConstructor")
+data class NetworkState private constructor(
+ val status: Status,
+ val msg: String? = null) {
+ companion object {
+ val LOADED = NetworkState(Status.SUCCESS)
+ val LOADING = NetworkState(Status.RUNNING)
+ fun error(msg: String?) = NetworkState(Status.FAILED, msg)
+ }
+}
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/RedditPostRepository.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/RedditPostRepository.kt
new file mode 100644
index 000000000..7bfa0d8f7
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/RedditPostRepository.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.repository
+
+import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost
+
+/**
+ * Common interface shared by the different repository implementations.
+ * Note: this only exists for sample purposes - typically an app would implement a repo once, either
+ * network+db, or network-only
+ */
+interface RedditPostRepository {
+ fun postsOfSubreddit(subReddit: String, pageSize: Int): Listing
+
+ enum class Type {
+ IN_MEMORY_BY_ITEM,
+ IN_MEMORY_BY_PAGE,
+ DB
+ }
+}
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inDb/DbRedditPostRepository.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inDb/DbRedditPostRepository.kt
new file mode 100644
index 000000000..f210ed26f
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inDb/DbRedditPostRepository.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.repository.inDb
+
+import android.arch.lifecycle.LiveData
+import android.arch.lifecycle.MutableLiveData
+import android.arch.lifecycle.Transformations
+import android.arch.paging.LivePagedListBuilder
+import android.support.annotation.MainThread
+import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi
+import com.android.example.paging.pagingwithnetwork.reddit.db.RedditDb
+import com.android.example.paging.pagingwithnetwork.reddit.repository.Listing
+import com.android.example.paging.pagingwithnetwork.reddit.repository.NetworkState
+import com.android.example.paging.pagingwithnetwork.reddit.repository.RedditPostRepository
+import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import java.util.concurrent.Executor
+
+/**
+ * Repository implementation that uses a database PagedList + a boundary callback to return a
+ * listing that loads in pages.
+ */
+class DbRedditPostRepository(
+ val db: RedditDb,
+ private val redditApi: RedditApi,
+ private val ioExecutor: Executor,
+ private val networkPageSize: Int = DEFAULT_NETWORK_PAGE_SIZE) : RedditPostRepository {
+ companion object {
+ private val DEFAULT_NETWORK_PAGE_SIZE = 10
+ }
+
+ /**
+ * Inserts the response into the database while also assigning position indices to items.
+ */
+ private fun insertResultIntoDb(subredditName: String, body: RedditApi.ListingResponse?) {
+ body!!.data.children.let { posts ->
+ db.runInTransaction {
+ val start = db.posts().getNextIndexInSubreddit(subredditName)
+ val items = posts.mapIndexed { index, child ->
+ child.data.indexInResponse = start + index
+ child.data
+ }
+ db.posts().insert(items)
+ }
+ }
+ }
+
+ /**
+ * When refresh is called, we simply run a fresh network request and when it arrives, clear
+ * the database table and insert all new items in a transaction.
+ *
+ * Since the PagedList already uses a database bound data source, it will automatically be
+ * updated after the database transaction is finished.
+ */
+ @MainThread
+ private fun refresh(subredditName: String): LiveData {
+ val networkState = MutableLiveData()
+ networkState.value = NetworkState.LOADING
+ redditApi.getTop(subredditName, networkPageSize).enqueue(
+ object : Callback {
+ override fun onFailure(call: Call, t: Throwable) {
+ // retrofit calls this on main thread so safe to call set value
+ networkState.value = NetworkState.error(t.message)
+ }
+
+ override fun onResponse(
+ call: Call,
+ response: Response) {
+ ioExecutor.execute {
+ db.runInTransaction {
+ db.posts().deleteBySubreddit(subredditName)
+ insertResultIntoDb(subredditName, response.body())
+ }
+ // since we are in bg thread now, post the result.
+ networkState.postValue(NetworkState.LOADED)
+ }
+ }
+ }
+ )
+ return networkState
+ }
+
+ /**
+ * Returns a Listing for the given subreddit.
+ */
+ @MainThread
+ override fun postsOfSubreddit(subredditName: String, pageSize: Int): Listing {
+ // create a boundary callback which will observe when the user reaches to the edges of
+ // the list and update the database with extra data.
+ val boundaryCallback = SubredditBoundaryCallback(
+ webservice = redditApi,
+ subredditName = subredditName,
+ handleResponse = this::insertResultIntoDb,
+ ioExecutor = ioExecutor,
+ networkPageSize = networkPageSize)
+ // create a data source factory from Room
+ val dataSourceFactory = db.posts().postsBySubreddit(subredditName)
+ val builder = LivePagedListBuilder(dataSourceFactory, pageSize)
+ .setBoundaryCallback(boundaryCallback)
+
+ // we are using a mutable live data to trigger refresh requests which eventually calls
+ // refresh method and gets a new live data. Each refresh request by the user becomes a newly
+ // dispatched data in refreshTrigger
+ val refreshTrigger = MutableLiveData()
+ val refreshState = Transformations.switchMap(refreshTrigger, {
+ refresh(subredditName)
+ })
+
+ return Listing(
+ pagedList = builder.build(),
+ networkState = boundaryCallback.networkState,
+ retry = {
+ boundaryCallback.helper.retryAllFailed()
+ },
+ refresh = {
+ refreshTrigger.value = null
+ },
+ refreshState = refreshState
+ )
+ }
+}
+
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inDb/SubredditBoundaryCallback.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inDb/SubredditBoundaryCallback.kt
new file mode 100644
index 000000000..2a6e21cbf
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inDb/SubredditBoundaryCallback.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.repository.inDb
+
+import android.arch.paging.PagedList
+import android.arch.paging.PagingRequestHelper
+import android.support.annotation.MainThread
+import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi
+import com.android.example.paging.pagingwithnetwork.reddit.util.createStatusLiveData
+import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import java.util.concurrent.Executor
+
+/**
+ * This boundary callback gets notified when user reaches to the edges of the list such that the
+ * database cannot provide any more data.
+ *
+ * The boundary callback might be called multiple times for the same direction so it does its own
+ * rate limiting using the PagingRequestHelper class.
+ */
+class SubredditBoundaryCallback(
+ private val subredditName: String,
+ private val webservice: RedditApi,
+ private val handleResponse: (String, RedditApi.ListingResponse?) -> Unit,
+ private val ioExecutor: Executor,
+ private val networkPageSize: Int)
+ : PagedList.BoundaryCallback() {
+
+ val helper = PagingRequestHelper(ioExecutor)
+ val networkState = helper.createStatusLiveData()
+
+ /**
+ * Database returned 0 items. We should query the backend for more items.
+ */
+ @MainThread
+ override fun onZeroItemsLoaded() {
+ helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) {
+ webservice.getTop(
+ subreddit = subredditName,
+ limit = networkPageSize)
+ .enqueue(createWebserviceCallback(it))
+ }
+ }
+
+ /**
+ * User reached to the end of the list.
+ */
+ @MainThread
+ override fun onItemAtEndLoaded(itemAtEnd: RedditPost) {
+ helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
+ webservice.getTopAfter(
+ subreddit = subredditName,
+ after = itemAtEnd.name,
+ limit = networkPageSize)
+ .enqueue(createWebserviceCallback(it))
+ }
+ }
+
+ /**
+ * every time it gets new items, boundary callback simply inserts them into the database and
+ * paging library takes care of refreshing the list if necessary.
+ */
+ private fun insertItemsIntoDb(
+ response: Response,
+ it: PagingRequestHelper.Request.Callback) {
+ ioExecutor.execute {
+ handleResponse(subredditName, response.body())
+ it.recordSuccess()
+ }
+ }
+
+ override fun onItemAtFrontLoaded(itemAtFront: RedditPost) {
+ // ignored, since we only ever append to what's in the DB
+ }
+
+ private fun createWebserviceCallback(it: PagingRequestHelper.Request.Callback)
+ : Callback {
+ return object : Callback {
+ override fun onFailure(
+ call: Call,
+ t: Throwable) {
+ it.recordFailure(t)
+ }
+
+ override fun onResponse(
+ call: Call,
+ response: Response) {
+ insertItemsIntoDb(response, it)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/InMemoryByItemRepository.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/InMemoryByItemRepository.kt
new file mode 100644
index 000000000..e466e1422
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/InMemoryByItemRepository.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.repository.inMemory.byItem
+
+import android.arch.lifecycle.Transformations
+import android.arch.paging.LivePagedListBuilder
+import android.arch.paging.PagedList
+import android.support.annotation.MainThread
+import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi
+import com.android.example.paging.pagingwithnetwork.reddit.repository.Listing
+import com.android.example.paging.pagingwithnetwork.reddit.repository.RedditPostRepository
+import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost
+import java.util.concurrent.Executor
+
+/**
+ * Repository implementation that returns a Listing that loads data directly from the network
+ * and uses the Item's name as the key to discover prev/next pages.
+ */
+class InMemoryByItemRepository(
+ private val redditApi: RedditApi,
+ private val networkExecutor: Executor) : RedditPostRepository {
+ @MainThread
+ override fun postsOfSubreddit(subredditName: String, pageSize: Int): Listing {
+ val sourceFactory = SubRedditDataSourceFactory(redditApi, subredditName, networkExecutor)
+ val pagedListConfig = PagedList.Config.Builder()
+ .setEnablePlaceholders(false)
+ .setInitialLoadSizeHint(pageSize * 2)
+ .setPageSize(pageSize)
+ .build()
+ val pagedList = LivePagedListBuilder(sourceFactory, pagedListConfig)
+ // provide custom executor for network requests, otherwise it will default to
+ // Arch Components' IO pool which is also used for disk access
+ .setFetchExecutor(networkExecutor)
+ .build()
+
+ val refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) {
+ it.initialLoad
+ }
+ return Listing(
+ pagedList = pagedList,
+ networkState = Transformations.switchMap(sourceFactory.sourceLiveData, {
+ it.networkState
+ }),
+ retry = {
+ sourceFactory.sourceLiveData.value?.retryAllFailed()
+ },
+ refresh = {
+ sourceFactory.sourceLiveData.value?.invalidate()
+ },
+ refreshState = refreshState
+ )
+ }
+}
+
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/ItemKeyedSubredditDataSource.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/ItemKeyedSubredditDataSource.kt
new file mode 100644
index 000000000..d459ba16c
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/ItemKeyedSubredditDataSource.kt
@@ -0,0 +1,144 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.repository.inMemory.byItem
+
+import android.arch.lifecycle.MutableLiveData
+import android.arch.paging.ItemKeyedDataSource
+import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi
+import com.android.example.paging.pagingwithnetwork.reddit.repository.NetworkState
+import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost
+import retrofit2.Call
+import retrofit2.Response
+import java.io.IOException
+import java.util.concurrent.Executor
+
+/**
+ * A data source that uses the "name" field of posts as the key for next/prev pages.
+ *
+ * Note that this is not the correct consumption of the Reddit API but rather shown here as an
+ * alternative implementation which might be more suitable for your backend.
+ * see PageKeyedSubredditDataSource for the other sample.
+ */
+class ItemKeyedSubredditDataSource(
+ private val redditApi: RedditApi,
+ private val subredditName: String,
+ private val retryExecutor: Executor)
+ : ItemKeyedDataSource() {
+ // keep a function reference for the retry event
+ private var retry: (() -> Any)? = null
+
+ /**
+ * There is no sync on the state because paging will always call loadInitial first then wait
+ * for it to return some success value before calling loadAfter and we don't support loadBefore
+ * in this example.
+ *
+ * See BoundaryCallback example for a more complete example on syncing multiple network states.
+ */
+ val networkState = MutableLiveData()
+
+ val initialLoad = MutableLiveData()
+ fun retryAllFailed() {
+ val prevRetry = retry
+ retry = null
+ prevRetry?.let {
+ retryExecutor.execute {
+ it.invoke()
+ }
+ }
+ }
+
+ override fun loadBefore(params: LoadParams, callback: LoadCallback) {
+ // ignored, since we only ever append to our initial load
+ }
+
+ override fun loadAfter(params: LoadParams, callback: LoadCallback) {
+ // set network value to loading.
+ networkState.postValue(NetworkState.LOADING)
+ // even though we are using async retrofit API here, we could also use sync
+ // it is just different to show that the callback can be called async.
+ redditApi.getTopAfter(subreddit = subredditName,
+ after = params.key,
+ limit = params.requestedLoadSize).enqueue(
+ object : retrofit2.Callback {
+ override fun onFailure(call: Call, t: Throwable) {
+ // keep a lambda for future retry
+ retry = {
+ loadAfter(params, callback)
+ }
+ // publish the error
+ networkState.postValue(NetworkState.error(t.message ?: "unknown err"))
+ }
+
+ override fun onResponse(
+ call: Call,
+ response: Response) {
+ if (response.isSuccessful) {
+ val items = response.body()?.data?.children?.map { it.data } ?: emptyList()
+ // clear retry since last request succeeded
+ retry = null
+ callback.onResult(items)
+ networkState.postValue(NetworkState.LOADED)
+ } else {
+ retry = {
+ loadAfter(params, callback)
+ }
+ networkState.postValue(
+ NetworkState.error("error code: ${response.code()}"))
+ }
+ }
+ }
+ )
+ }
+
+ /**
+ * The name field is a unique identifier for post items.
+ * (no it is not the title of the post :) )
+ * https://www.reddit.com/dev/api
+ */
+ override fun getKey(item: RedditPost): String = item.name
+
+ override fun loadInitial(
+ params: LoadInitialParams,
+ callback: LoadInitialCallback) {
+ val request = redditApi.getTop(
+ subreddit = subredditName,
+ limit = params.requestedLoadSize
+ )
+ // update network states.
+ // we also provide an initial load state to the listeners so that the UI can know when the
+ // very first list is loaded.
+ networkState.postValue(NetworkState.LOADING)
+ initialLoad.postValue(NetworkState.LOADING)
+
+ // triggered by a refresh, we better execute sync
+ try {
+ val response = request.execute()
+ val items = response.body()?.data?.children?.map { it.data } ?: emptyList()
+ retry = null
+ networkState.postValue(NetworkState.LOADED)
+ initialLoad.postValue(NetworkState.LOADED)
+ callback.onResult(items)
+ } catch (ioException: IOException) {
+ retry = {
+ loadInitial(params, callback)
+ }
+ val error = NetworkState.error(ioException.message ?: "unknown error")
+ networkState.postValue(error)
+ initialLoad.postValue(error)
+ }
+ }
+}
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/SubRedditDataSourceFactory.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/SubRedditDataSourceFactory.kt
new file mode 100644
index 000000000..2089c6305
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/SubRedditDataSourceFactory.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.repository.inMemory.byItem
+
+import android.arch.lifecycle.MutableLiveData
+import android.arch.paging.DataSource
+import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi
+import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost
+import java.util.concurrent.Executor
+
+/**
+ * A simple data source factory which also provides a way to observe the last created data source.
+ * This allows us to channel its network request status etc back to the UI. See the Listing creation
+ * in the Repository class.
+ */
+class SubRedditDataSourceFactory(
+ private val redditApi: RedditApi,
+ private val subredditName: String,
+ private val retryExecutor: Executor) : DataSource.Factory() {
+ val sourceLiveData = MutableLiveData()
+ override fun create(): DataSource {
+ val source = ItemKeyedSubredditDataSource(redditApi, subredditName, retryExecutor)
+ sourceLiveData.postValue(source)
+ return source
+ }
+}
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/InMemoryByPageKeyRepository.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/InMemoryByPageKeyRepository.kt
new file mode 100644
index 000000000..6d5a18f10
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/InMemoryByPageKeyRepository.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.repository.inMemory.byPage
+
+import android.arch.lifecycle.Transformations
+import android.arch.paging.LivePagedListBuilder
+import android.support.annotation.MainThread
+import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi
+import com.android.example.paging.pagingwithnetwork.reddit.repository.Listing
+import com.android.example.paging.pagingwithnetwork.reddit.repository.RedditPostRepository
+import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost
+import java.util.concurrent.Executor
+
+/**
+ * Repository implementation that returns a Listing that loads data directly from network by using
+ * the previous / next page keys returned in the query.
+ */
+class InMemoryByPageKeyRepository(private val redditApi: RedditApi,
+ private val networkExecutor: Executor) : RedditPostRepository {
+ @MainThread
+ override fun postsOfSubreddit(subredditName: String, pageSize: Int): Listing {
+ val sourceFactory = SubRedditDataSourceFactory(redditApi, subredditName, networkExecutor)
+
+ val livePagedList = LivePagedListBuilder(sourceFactory, pageSize)
+ // provide custom executor for network requests, otherwise it will default to
+ // Arch Components' IO pool which is also used for disk access
+ .setFetchExecutor(networkExecutor)
+ .build()
+
+ val refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) {
+ it.initialLoad
+ }
+ return Listing(
+ pagedList = livePagedList,
+ networkState = Transformations.switchMap(sourceFactory.sourceLiveData, {
+ it.networkState
+ }),
+ retry = {
+ sourceFactory.sourceLiveData.value?.retryAllFailed()
+ },
+ refresh = {
+ sourceFactory.sourceLiveData.value?.invalidate()
+ },
+ refreshState = refreshState
+ )
+ }
+}
+
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/PageKeyedSubredditDataSource.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/PageKeyedSubredditDataSource.kt
new file mode 100644
index 000000000..e53c6e84b
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/PageKeyedSubredditDataSource.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.repository.inMemory.byPage
+
+import android.arch.lifecycle.MutableLiveData
+import android.arch.paging.PageKeyedDataSource
+import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi
+import com.android.example.paging.pagingwithnetwork.reddit.repository.NetworkState
+import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost
+import retrofit2.Call
+import retrofit2.Response
+import java.io.IOException
+import java.util.concurrent.Executor
+
+/**
+ * A data source that uses the before/after keys returned in page requests.
+ *
+ * See ItemKeyedSubredditDataSource
+ */
+class PageKeyedSubredditDataSource(
+ private val redditApi: RedditApi,
+ private val subredditName: String,
+ private val retryExecutor: Executor) : PageKeyedDataSource() {
+
+ // keep a function reference for the retry event
+ private var retry: (() -> Any)? = null
+
+ /**
+ * There is no sync on the state because paging will always call loadInitial first then wait
+ * for it to return some success value before calling loadAfter.
+ */
+ val networkState = MutableLiveData()
+
+ val initialLoad = MutableLiveData()
+
+ fun retryAllFailed() {
+ val prevRetry = retry
+ retry = null
+ prevRetry?.let {
+ retryExecutor.execute {
+ it.invoke()
+ }
+ }
+ }
+
+ override fun loadBefore(
+ params: LoadParams,
+ callback: LoadCallback) {
+ // ignored, since we only ever append to our initial load
+ }
+
+ override fun loadAfter(params: LoadParams, callback: LoadCallback) {
+ networkState.postValue(NetworkState.LOADING)
+ redditApi.getTopAfter(subreddit = subredditName,
+ after = params.key,
+ limit = params.requestedLoadSize).enqueue(
+ object : retrofit2.Callback {
+ override fun onFailure(call: Call, t: Throwable) {
+ retry = {
+ loadAfter(params, callback)
+ }
+ networkState.postValue(NetworkState.error(t.message ?: "unknown err"))
+ }
+
+ override fun onResponse(
+ call: Call,
+ response: Response) {
+ if (response.isSuccessful) {
+ val data = response.body()?.data
+ val items = data?.children?.map { it.data } ?: emptyList()
+ retry = null
+ callback.onResult(items, data?.after)
+ networkState.postValue(NetworkState.LOADED)
+ } else {
+ retry = {
+ loadAfter(params, callback)
+ }
+ networkState.postValue(
+ NetworkState.error("error code: ${response.code()}"))
+ }
+ }
+ }
+ )
+ }
+
+ override fun loadInitial(
+ params: LoadInitialParams,
+ callback: LoadInitialCallback) {
+ val request = redditApi.getTop(
+ subreddit = subredditName,
+ limit = params.requestedLoadSize
+ )
+ networkState.postValue(NetworkState.LOADING)
+ initialLoad.postValue(NetworkState.LOADING)
+
+ // triggered by a refresh, we better execute sync
+ try {
+ val response = request.execute()
+ val data = response.body()?.data
+ val items = data?.children?.map { it.data } ?: emptyList()
+ retry = null
+ networkState.postValue(NetworkState.LOADED)
+ initialLoad.postValue(NetworkState.LOADED)
+ callback.onResult(items, data?.before, data?.after)
+ } catch (ioException: IOException) {
+ retry = {
+ loadInitial(params, callback)
+ }
+ val error = NetworkState.error(ioException.message ?: "unknown error")
+ networkState.postValue(error)
+ initialLoad.postValue(error)
+ }
+ }
+}
\ No newline at end of file
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/SubRedditDataSourceFactory.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/SubRedditDataSourceFactory.kt
new file mode 100644
index 000000000..a8dd0fa51
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/SubRedditDataSourceFactory.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.repository.inMemory.byPage
+
+import android.arch.lifecycle.MutableLiveData
+import android.arch.paging.DataSource
+import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi
+import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost
+import java.util.concurrent.Executor
+
+/**
+ * A simple data source factory which also provides a way to observe the last created data source.
+ * This allows us to channel its network request status etc back to the UI. See the Listing creation
+ * in the Repository class.
+ */
+class SubRedditDataSourceFactory(
+ private val redditApi: RedditApi,
+ private val subredditName: String,
+ private val retryExecutor: Executor) : DataSource.Factory() {
+ val sourceLiveData = MutableLiveData()
+ override fun create(): DataSource {
+ val source = PageKeyedSubredditDataSource(redditApi, subredditName, retryExecutor)
+ sourceLiveData.postValue(source)
+ return source
+ }
+}
diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/NetworkStateItemViewHolder.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/NetworkStateItemViewHolder.kt
new file mode 100644
index 000000000..6d46750f1
--- /dev/null
+++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/NetworkStateItemViewHolder.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.android.example.paging.pagingwithnetwork.reddit.ui
+
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ProgressBar
+import android.widget.TextView
+import com.android.example.paging.pagingwithnetwork.R
+import com.android.example.paging.pagingwithnetwork.reddit.repository.NetworkState
+import com.android.example.paging.pagingwithnetwork.reddit.repository.Status.FAILED
+import com.android.example.paging.pagingwithnetwork.reddit.repository.Status.RUNNING
+
+/**
+ * A View Holder that can display a loading or have click action.
+ * It is used to show the network state of paging.
+ */
+class NetworkStateItemViewHolder(view: View,
+ private val retryCallback: () -> Unit)
+ : RecyclerView.ViewHolder(view) {
+ private val progressBar = view.findViewById(R.id.progress_bar)
+ private val retry = view.findViewById