From c02e1fc9c0458ab0b2c981479ad3d666e82c9feb Mon Sep 17 00:00:00 2001 From: Dmitrii Nikitin Date: Tue, 5 Sep 2017 01:59:31 +0300 Subject: [PATCH] Add SQLDelight interaction example (#814) --- build.gradle | 2 + storio-sample-app/build.gradle | 4 + .../src/main/AndroidManifest.xml | 4 + .../storio2/sample/AppComponent.java | 3 + .../storio2/sample/db/DbOpenHelper.java | 3 + .../ManyToManyActivity.java | 3 +- .../many_to_many_sample/PersonsAdapter.java | 12 +- .../sample/sqldelight/CustomersAdapter.java | 70 ++++++++ .../storio2/sample/sqldelight/SQLUtils.java | 72 +++++++++ .../sample/sqldelight/SqlDelightActivity.java | 150 ++++++++++++++++++ .../sample/sqldelight/entities/Customer.java | 64 ++++++++ .../sample/ui/activity/MainActivity.java | 11 +- .../sample/ui/adapter/TweetsAdapter.java | 21 ++- .../sample/ui/fragment/TweetsFragment.java | 11 +- .../main/res/layout/activity_sqldelight.xml | 79 +++++++++ .../src/main/res/layout/fragment_tweets.xml | 2 +- .../main/res/layout/list_item_customer.xml | 29 ++++ .../src/main/res/layout/list_item_tweet.xml | 2 +- storio-sample-app/src/main/res/menu/main.xml | 4 + .../src/main/res/values/strings.xml | 6 +- .../sample/sqldelight/entities/Customer.sq | 14 ++ 21 files changed, 545 insertions(+), 21 deletions(-) create mode 100644 storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/CustomersAdapter.java create mode 100644 storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/SQLUtils.java create mode 100644 storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/SqlDelightActivity.java create mode 100644 storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/entities/Customer.java create mode 100644 storio-sample-app/src/main/res/layout/activity_sqldelight.xml create mode 100644 storio-sample-app/src/main/res/layout/list_item_customer.xml create mode 100644 storio-sample-app/src/main/sqldelight/com/pushtorefresh/storio2/sample/sqldelight/entities/Customer.sq diff --git a/build.gradle b/build.gradle index c080b87af..cd38385aa 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,8 @@ buildscript { // Automatic releases to Sonatype. classpath 'io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.8.0' + classpath 'com.squareup.sqldelight:gradle-plugin:0.6.1' + // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/storio-sample-app/build.gradle b/storio-sample-app/build.gradle index 53c197459..975efdb0e 100644 --- a/storio-sample-app/build.gradle +++ b/storio-sample-app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'com.squareup.sqldelight' android { compileSdkVersion rootProject.ext.compileSdkVersion @@ -68,6 +69,9 @@ dependencies { compile libraries.recyclerView compile libraries.timber + provided libraries.autoParcel + annotationProcessor libraries.autoParcelProcessor + debugCompile libraries.leakCanary releaseCompile libraries.leakCanaryNoOp diff --git a/storio-sample-app/src/main/AndroidManifest.xml b/storio-sample-app/src/main/AndroidManifest.xml index e43d4a37f..9d2652b87 100644 --- a/storio-sample-app/src/main/AndroidManifest.xml +++ b/storio-sample-app/src/main/AndroidManifest.xml @@ -30,6 +30,10 @@ android:name="com.pushtorefresh.storio2.sample.many_to_many_sample.ManyToManyActivity" android:label="@string/many_to_many" /> + + { + @NonNull + private final LayoutInflater layoutInflater; + @NonNull private List persons = Collections.emptyList(); @NonNull private final Callbacks callbacks; - public PersonsAdapter(@NonNull Callbacks callbacks) { + public PersonsAdapter(@NonNull LayoutInflater layoutInflater, @NonNull Callbacks callbacks) { + this.layoutInflater = layoutInflater; this.callbacks = callbacks; } @@ -40,10 +44,10 @@ public int getItemCount() { } @Override - public @NonNull - ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_person, parent, false), callbacks); + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View itemView = layoutInflater.inflate(R.layout.list_item_person, parent, false); + return new ViewHolder(itemView, callbacks); } @Override diff --git a/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/CustomersAdapter.java b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/CustomersAdapter.java new file mode 100644 index 000000000..7fe7e0651 --- /dev/null +++ b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/CustomersAdapter.java @@ -0,0 +1,70 @@ +package com.pushtorefresh.storio2.sample.sqldelight; + +import android.annotation.SuppressLint; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.pushtorefresh.storio2.sample.R; +import com.pushtorefresh.storio2.sample.sqldelight.entities.Customer; + +import java.util.Collections; +import java.util.List; + +import butterknife.Bind; +import butterknife.ButterKnife; + +public class CustomersAdapter extends RecyclerView.Adapter { + + @NonNull + private final LayoutInflater layoutInflater; + + @NonNull + private List customers = Collections.emptyList(); + + public CustomersAdapter(@NonNull LayoutInflater layoutInflater) { + this.layoutInflater = layoutInflater; + } + + public void setCustomers(@NonNull List customers) { + this.customers = customers; + notifyDataSetChanged(); + } + + @Override + public int getItemCount() { + return customers.size(); + } + + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View itemView = layoutInflater.inflate(R.layout.list_item_customer, parent, false); + return new ViewHolder(itemView); + } + + @SuppressLint("SetTextI18n") + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + final Customer customer = customers.get(position); + + holder.name.setText(String.format("%s %s", customer.name(), customer.surname())); + holder.city.setText(customer.city()); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + + @Bind(R.id.list_item_customer_name) + TextView name; + + @Bind(R.id.list_item_customer_city) + TextView city; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + } + } +} diff --git a/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/SQLUtils.java b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/SQLUtils.java new file mode 100644 index 000000000..ce3463556 --- /dev/null +++ b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/SQLUtils.java @@ -0,0 +1,72 @@ +package com.pushtorefresh.storio2.sample.sqldelight; + +import android.content.ContentValues; +import android.database.Cursor; +import android.support.annotation.NonNull; + +import com.pushtorefresh.storio2.sqlite.StorIOSQLite; +import com.pushtorefresh.storio2.sqlite.operations.put.PutResolver; +import com.pushtorefresh.storio2.sqlite.operations.put.PutResult; +import com.pushtorefresh.storio2.sqlite.queries.InsertQuery; +import com.pushtorefresh.storio2.sqlite.queries.RawQuery; +import com.squareup.sqldelight.RowMapper; +import com.squareup.sqldelight.SqlDelightStatement; + +import java.util.ArrayList; +import java.util.List; + +import static com.pushtorefresh.storio2.sqlite.operations.put.PutResult.newInsertResult; + +public final class SQLUtils { + + private SQLUtils() { + } + + @NonNull + public static PutResolver makeSimpleContentValuesInsertPutResolver(@NonNull final String tableName) { + return new PutResolver() { + @NonNull + final InsertQuery insert = InsertQuery.builder() + .table(tableName) + .build(); + + @NonNull + @Override + public PutResult performPut(@NonNull StorIOSQLite storIOSQLite, @NonNull ContentValues contentValues) { + final long insertedId = storIOSQLite.lowLevel().insert(insert, contentValues); + return newInsertResult(insertedId, tableName); + } + }; + } + + @NonNull + public static RawQuery makeReadQuery(@NonNull SqlDelightStatement statement) { + return RawQuery.builder() + .query(statement.statement) + .args(statement.args) + .observesTables(statement.tables) + .build(); + } + + @NonNull + public static RawQuery makeWriteQuery(@NonNull SqlDelightStatement statement) { + return RawQuery.builder() + .query(statement.statement) + .args(statement.args) + .affectsTables(statement.tables) + .build(); + } + + @NonNull + public static List mapFromCursor(@NonNull Cursor cursor, @NonNull RowMapper mapper) { + try { + final List result = new ArrayList(cursor.getCount()); + while (cursor.moveToNext()) { + result.add(mapper.map(cursor)); + } + return result; + } finally { + cursor.close(); + } + } +} diff --git a/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/SqlDelightActivity.java b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/SqlDelightActivity.java new file mode 100644 index 000000000..4f79b3074 --- /dev/null +++ b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/SqlDelightActivity.java @@ -0,0 +1,150 @@ +package com.pushtorefresh.storio2.sample.sqldelight; + +import android.content.ContentValues; +import android.database.Cursor; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; + +import com.pushtorefresh.storio2.sample.R; +import com.pushtorefresh.storio2.sample.SampleApp; +import com.pushtorefresh.storio2.sample.sqldelight.entities.Customer; +import com.pushtorefresh.storio2.sample.ui.UiStateController; +import com.pushtorefresh.storio2.sample.ui.activity.BaseActivity; +import com.pushtorefresh.storio2.sqlite.StorIOSQLite; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; + +import butterknife.Bind; +import butterknife.ButterKnife; +import butterknife.OnClick; +import rx.Subscription; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Func1; +import timber.log.Timber; + +import static com.pushtorefresh.storio2.sample.sqldelight.SQLUtils.makeReadQuery; +import static com.pushtorefresh.storio2.sample.sqldelight.SQLUtils.mapFromCursor; +import static com.pushtorefresh.storio2.sample.ui.Toasts.safeShowShortToast; +import static rx.android.schedulers.AndroidSchedulers.mainThread; + +public class SqlDelightActivity extends BaseActivity { + + @Inject + StorIOSQLite storIOSQLite; + + UiStateController uiStateController; + + @Bind(R.id.customers_recycler_view) + RecyclerView recyclerView; + + CustomersAdapter customersAdapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_sqldelight); + SampleApp.get(this).appComponent().inject(this); + customersAdapter = new CustomersAdapter(LayoutInflater.from(this)); + + ButterKnife.bind(this); + + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setAdapter(customersAdapter); + recyclerView.setHasFixedSize(true); + recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); + + uiStateController = new UiStateController.Builder() + .withLoadingUi(findViewById(R.id.customer_loading_ui)) + .withErrorUi(findViewById(R.id.customers_error_ui)) + .withEmptyUi(findViewById(R.id.customers_empty_ui)) + .withContentUi(recyclerView) + .build(); + } + + @Override + public void onStart() { + super.onStart(); + loadData(); + } + + void loadData() { + uiStateController.setUiStateLoading(); + + final Subscription subscription = storIOSQLite + .get() + .cursor() + .withQuery(makeReadQuery(Customer.FACTORY.select_all())) + .prepare() + .asRxObservable() + .map(new Func1>() { + @Override + public List call(Cursor cursor) { + return mapFromCursor(cursor, Customer.CURSOR_MAPPER); + } + }) + .observeOn(mainThread()) + .subscribe(new Action1>() { + @Override + public void call(List customers) { + if (customers.isEmpty()) { + uiStateController.setUiStateEmpty(); + customersAdapter.setCustomers(Collections.emptyList()); + } else { + uiStateController.setUiStateContent(); + customersAdapter.setCustomers(customers); + } + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + Timber.e(throwable, "loadData()"); + uiStateController.setUiStateError(); + customersAdapter.setCustomers(Collections.emptyList()); + } + }); + unsubscribeOnStop(subscription); + } + + @OnClick(R.id.customers_empty_ui_add_button) + void addContent() { + final List customers = new ArrayList(); + + customers.add(Customer.builder().name("Elon").surname("Musk").city("Boring").build()); + customers.add(Customer.builder().name("Jake").surname("Wharton").city("Pittsburgh").build()); + + List contentValues = new ArrayList(customers.size()); + for (Customer customer : customers) { + contentValues.add(Customer.FACTORY.marshal(customer).asContentValues()); + } + + storIOSQLite + .put() + .contentValues(contentValues) + .withPutResolver(Customer.CV_PUT_RESOLVER) + .prepare() + .asRxCompletable() + .observeOn(mainThread()) + .subscribe( + new Action0() { + @Override + public void call() { + // no impl required + } + }, + new Action1() { + @Override + public void call(Throwable throwable) { + safeShowShortToast(SqlDelightActivity.this, R.string.common_error); + } + }); + } +} diff --git a/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/entities/Customer.java b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/entities/Customer.java new file mode 100644 index 000000000..5eff38684 --- /dev/null +++ b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/sqldelight/entities/Customer.java @@ -0,0 +1,64 @@ +package com.pushtorefresh.storio2.sample.sqldelight.entities; + + +import android.content.ContentValues; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.pushtorefresh.storio2.sqlite.operations.put.PutResolver; + +import auto.parcel.AutoParcel; + +import static com.pushtorefresh.storio2.sample.sqldelight.SQLUtils.makeSimpleContentValuesInsertPutResolver; + +@AutoParcel +public abstract class Customer implements CustomerModel { + + @NonNull + public static final Factory FACTORY = new Factory(new Creator() { + @Override + public Customer create( + @Nullable Long id, + @NonNull String name, + @NonNull String surname, + @NonNull String city + ) { + return builder() + .id(id) + .name(name) + .surname(surname) + .city(city) + .build(); + } + }); + + @NonNull + public static final Mapper CURSOR_MAPPER = new Mapper(FACTORY); + + @NonNull + public static final PutResolver CV_PUT_RESOLVER = makeSimpleContentValuesInsertPutResolver(TABLE_NAME); + + @NonNull + public static Builder builder() { + return new AutoParcel_Customer.Builder(); + } + + @AutoParcel.Builder + public interface Builder { + + @NonNull + Builder id(@Nullable Long id); + + @NonNull + Builder name(@NonNull String name); + + @NonNull + Builder surname(@NonNull String surname); + + @NonNull + Builder city(@NonNull String city); + + @NonNull + Customer build(); + } +} diff --git a/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/ui/activity/MainActivity.java b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/ui/activity/MainActivity.java index f3df6ac7f..bab8600f8 100644 --- a/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/ui/activity/MainActivity.java +++ b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/ui/activity/MainActivity.java @@ -9,6 +9,7 @@ import com.pushtorefresh.storio2.sample.R; import com.pushtorefresh.storio2.sample.many_to_many_sample.ManyToManyActivity; +import com.pushtorefresh.storio2.sample.sqldelight.SqlDelightActivity; import com.pushtorefresh.storio2.sample.ui.Toasts; import com.pushtorefresh.storio2.sample.ui.activity.db.TweetsSampleActivity; @@ -33,9 +34,13 @@ public boolean onCreateOptionsMenu(@NonNull Menu menu) { @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == R.id.many_to_many) { - startActivity(new Intent(this, ManyToManyActivity.class)); - return true; + switch (item.getItemId()) { + case R.id.many_to_many: + startActivity(new Intent(this, ManyToManyActivity.class)); + return true; + case R.id.sqldelight: + startActivity(new Intent(this, SqlDelightActivity.class)); + return true; } return super.onOptionsItemSelected(item); } diff --git a/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/ui/adapter/TweetsAdapter.java b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/ui/adapter/TweetsAdapter.java index 49b5bf3a7..9c3913731 100644 --- a/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/ui/adapter/TweetsAdapter.java +++ b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/ui/adapter/TweetsAdapter.java @@ -2,7 +2,6 @@ import android.annotation.SuppressLint; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; @@ -17,24 +16,34 @@ import butterknife.Bind; import butterknife.ButterKnife; +import static java.util.Collections.emptyList; + public class TweetsAdapter extends RecyclerView.Adapter { - private List tweets; + @NonNull + private final LayoutInflater layoutInflater; + + @NonNull + private List tweets = emptyList(); + + public TweetsAdapter(@NonNull LayoutInflater layoutInflater) { + this.layoutInflater = layoutInflater; + } - public void setTweets(@Nullable List tweets) { + public void setTweets(@NonNull List tweets) { this.tweets = tweets; notifyDataSetChanged(); } @Override public int getItemCount() { - return tweets == null ? 0 : tweets.size(); + return tweets.size(); } @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new ViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.list_item_tweet, parent, false)); + View itemView = layoutInflater.inflate(R.layout.list_item_tweet, parent, false); + return new ViewHolder(itemView); } @SuppressLint("SetTextI18n") diff --git a/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/ui/fragment/TweetsFragment.java b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/ui/fragment/TweetsFragment.java index 6fe99c31a..073cb5a5b 100644 --- a/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/ui/fragment/TweetsFragment.java +++ b/storio-sample-app/src/main/java/com/pushtorefresh/storio2/sample/ui/fragment/TweetsFragment.java @@ -3,6 +3,7 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.app.FragmentActivity; import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.LinearLayoutManager; @@ -22,6 +23,7 @@ import com.pushtorefresh.storio2.sqlite.operations.put.PutResults; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javax.inject.Inject; @@ -55,8 +57,9 @@ public class TweetsFragment extends BaseFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - SampleApp.get(getActivity()).appComponent().inject(this); - tweetsAdapter = new TweetsAdapter(); + final FragmentActivity activity = getActivity(); + SampleApp.get(activity).appComponent().inject(this); + tweetsAdapter = new TweetsAdapter(LayoutInflater.from(activity)); new Relations(storIOSQLite).getTweetWithUser(); } @@ -112,7 +115,7 @@ public void call(List tweets) { // So you just need to check if it's empty or not if (tweets.isEmpty()) { uiStateController.setUiStateEmpty(); - tweetsAdapter.setTweets(null); + tweetsAdapter.setTweets(Collections.emptyList()); } else { uiStateController.setUiStateContent(); tweetsAdapter.setTweets(tweets); @@ -125,7 +128,7 @@ public void call(Throwable throwable) { // You can prevent crash of the application via error handler Timber.e(throwable, "reloadData()"); uiStateController.setUiStateError(); - tweetsAdapter.setTweets(null); + tweetsAdapter.setTweets(Collections.emptyList()); } }); diff --git a/storio-sample-app/src/main/res/layout/activity_sqldelight.xml b/storio-sample-app/src/main/res/layout/activity_sqldelight.xml new file mode 100644 index 000000000..fefb4a7e5 --- /dev/null +++ b/storio-sample-app/src/main/res/layout/activity_sqldelight.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + +