Upload
droidcon-berlin
View
427
Download
0
Embed Size (px)
Citation preview
Alex Yanchenko @ de.droidcon.com/2014
DroidParts Explained
● SQL operations.● JSON (de)serialization.● HTTP interactions.● Loading, caching, displaying images.● Performing work in background.● Logging.● Dependency management.● Publish/subscribe (aka Event Bus).● Misc repeating tasks.
Common Tasks
Existing Solutions
Cursor query(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit);
static final String TAG = InEvery.class.getSimpleName();
Log.wtf(TAG, "Y TAG?!");
Picasso.with(context).load(url).placeholder(R.drawable.ph) .error(R.drawable.err).into(imageView);
Repetetive code:
Not using a builder when should:
Using a builder when shouldn't:
(Fluent kills inheritance)
Existing Solutions
Gson gson = new GsonBuilder().excludeFieldsWithModifiers( Modifier.STATIC).create();// By default, if you mark a field as transient, it will be excluded.
The @DatabaseField annotation can have the following fields:
(27 ot them)
Entity user = schema.addEntity("User");user.addIdProperty();user.addStringProperty("name");user.implementsSerializable();// Then generate .java files & paste to your project.
A mess to maintain:
Abusing built-in language features:
Many features remain unused on mobile:
Existing Solutions
<com.android.volley.toolbox.NetworkImageView android:id="@+id/pic" />
NetworkImageView pic = (NetworkImageView)view .findViewById(R.id.pic);pic.setImageUrl("http://example.com/pic.png", mImageLoader); // OMG
AsyncHttpClient client = new AsyncHttpClient();client.get("http://example.com/timeline.json", new JsonHttpResponseHandler() { @Override public void onSuccess(JSONArray response) { System.out.println( "Keep calm & process JSON on the UI thread."); }});
Ignoring separation of concerns #2:
Ignoring separation of concerns:
Existing Solutions
Coded past “Ballmer Peak”:@Headers({ "Accept: application/vnd.github.v3.full+json", "User-Agent: Retrofit-Sample-App"})@GET("/group/{id}/users")List<User> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);
©xkcd
● Help handle most common tasks?
● Uniform API?
● Simple to make easy things easy?
● Flexible to make hard things possible?
● Won't reinvent Java, OO friendly?
● Like Django, but for Android?
What If?
Yes.
What If?
● Dependency Inection.● EventBus.● JSON (de)serialization.● Object-Relational Mapping.● AsyncTask, IntentService.● RESTClient.● ImageFetcher.● Logger.● Misc.
DroidParts Parts
250kB, 8kLOC (v.2.0.4)
App Blocks
HTTP/REST JSON SQL/ORM
IntentService
Activity
Fragment
View
AsyncTask
EventBusDependency
Injection
AndroidManifest.xml:
DI Setup
<meta-data android:name="droidparts_dependency_provider" android:value=".DependencyProvider" />
public class DependencyProvider extends AbstractDependencyProvider {
public DependencyProvider(Context ctx) { super(ctx); }}
DependencyProvider.java:
DependencyProvider private final DBOpenHelper dbOpenHelper; private PrefsManager prefsManager;
public DependencyProvider(Context ctx) { super(ctx); dbOpenHelper = new DBOpenHelper(ctx); }
@Override public AbstractDBOpenHelper getDBOpenHelper() { return dbOpenHelper; }
public PrefsManager getPrefsManager(Context ctx) { if (prefsManager == null) { prefsManager = new PrefsManager(ctx); } return prefsManager; }
public DialogFactory getDialogFactory(Context ctx) { return new DialogFactory(ctx); }
Injection Annotations
@InjectDependency - from DependencyProvider@InjectParentActivity - in a Fragment
@InjectView(int id, boolean click) - from layout@InjectFragment(int id) - in an Activity
@InjectResource(int value) - strings, images...@InjectSystemService - TelephonyManager, ...
@InjectBundleExtra(String key, boolean optional)- from Intent in an Activity or from args in a Fragment
Injection in an Activityclass MyActivity extends Activity implements OnClickListener {
@InjectSystemService private LayoutInflater layoutInflater;
@InjectDependency private DialogFactory dialogFactory;
@InjectView(id = R.id.button_add, click = true) private Button addButton;
@Override public void onPreInject() { setContentView(R.layout.activity_my); }
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // <-- Injection }
@Override public void onClick(View v) { if (v == addButton) { // TODO } }}
Base Activities
class MyActivity extends Activity {
@Override public void onPreInject() { setContentView(R.layout.activity_my); }}
package org.droidparts.activity;
class Activity extends android.app.Activity {
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); onPreInject(); Injector.inject(this); // <-- Magic }}
App Activity:
DroidParts Activity:
Base Activities & Fragments
DroidParts:● 3.0+ with ActionBar features● Pre-3.0, no ActionBar
(.legacy.*)
DroidParts Support:● Pre-3.0 with Android Support Library
(.support.v4.*)● Pre-3.0 with ActionBarSherlock
(.sherlock.*)● Pre-3.0 with Android Support Library ActionBar
(.support.v7.*)
Base Activities & Fragments
No “DroidParts” or “DP” prefix.
Nice features:
→
SingleFragmentActivityTabbedFragmentActivity
Activity Factory Methods
class PostListActivity extends Activity {
private static final String EXTRA_POSTS = "posts";
public static Intent getIntent(Context ctx, ArrayList<Post> posts) { Intent intent = new Intent(ctx, MyActivity.class); intent.putExtra(EXTRA_POSTS, posts); return intent; }
@InjectBundleExtra(key = EXTRA_POSTS) private ArrayList<Post> posts;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // TODO display posts. }}
Fragment Listener
class PostListActivity extends Activity implements PostListFragment.Listener {
@InjectFragment private PostListFragment fragment;
@Override public void onPreInject() { setContentView(R.layout.layout_with_fragments); }}
class PostListFragment extends ListFragment {
public interface Listener { void doShowImageDetail(int position); }
@InjectParentActivity private Listener listener;}
EventBus(aka MessageBus)
● Message: Name + optional payload Object.(not a custom class)
● Sent from any thread, delivered on the UI thread.
EventBus
interface EventReceiver<T> {
void onEvent(String name, T data);
}
EventBus.postEvent(String name)// orEventBus.postEventSticky(String name, Object data)
Send:
Receive:EventBus.registerReceiver(EventReceiver<T> receiver, String... eventNames)
// TODO Remember to unregister.
EventBus
@ReceiveEvents(name = { "MESSAGE_SENT", "MESSAGE_RECEIVED" })private void onMessageEvent(String eventName) { // TODO process 2 types of events without data}
@ReceiveEventsprivate void onAnyEvent(String eventName, Object eventData) { // TODO process any event with optional data}
EventBus.postEvent(String name)// orEventBus.postEventSticky(String name, Object data)
Send:
Receive in an injected class:
Data Layer
JSON ORM
IntentService AsyncTask
JSON, ORM
// JSONclass JSONSerializer<ModelType extends Model> {}
// ORMclass EntityManager<EntityType extends Entity> {}
// JSONclass Model implements Serializable {}
// ORMclass Entity extends Model {
@Column(name = "_id") public long id;}
Base Model Classes:
Managers:
JSON
@Key(Sting name, boolean optional)
Using org.json.* under the hood.
class JSONSerializer<ModelType extends Model> {
JSONObject serialize(ModelType item) JSONArray serialize(Collection<ModelType> items)
ModelType deserialize(JSONObject obj) ArrayList<ModelType> deserialize(JSONArray arr)}
Annotation:
Manager:
JSON{ "author": "Alex" "address": "http://droidparts.org", "posts": [{ "published": 1398970584375, "title": "Title", }],}
class Blog extends Model { @Key public String author; @Key(name="address") public Uri uri; @Key public Post[] posts;}
class Post extends Model { @Key public Date published; @Key(name="title", optional=false) public String title; @Key(optional = true) public String text = "";}
JSON
{ "sub_obj": { "str": "val" }}
@Key(name = "sub_obj" + Key.SUB + "str")String str;
@Overridepublic Blog deserialize(JSONObject obj) throws JSONException { Blog blog = super.deserialize(obj);
for (Post post : blog.posts) { post.blog = blog; } return blog;}
Override serialize()/deserialize() for tweaking:
Accessing nested object's property:
ORM
@Table(name = "posts")public class Post extends Entity {
@Column public Date published; @Column(unique = true) public String title; @Column(nullable = true) public String text = "";
@Column(eager = true) public Blog blog;
}
@Table(String name)
@Column(String name, boolean nullable, boolean unique, boolean eager)
Annotations:
Class example:
ORM
class Blog extends Entity {
List<Post> getPosts() {
EntityManager<Post> em = new EntityManager<Post>( Post.class, Injector.getApplicationContext());
Select<Post> select = em.select().where("blog_id", Is.EQUAL, this.id);
return em.readAll(select); }
}
Reading one-to-many:
EntityManager: C_UD
class Post extends Entity {}
EntityManager<Post> em = new EntityManager<Post>(Post.class, ctx);// Usually subclass & add helper methods.
Post post = new Post();
em.create(post);
assert(post.id != 0);
em.update(post);
em.createOrUpdate(post);
int postsDeleted = em.delete() .where("year", Is.LESS, 2013) .execute();
EntityManager: _R__
EntityManager<Post> em;
// 1Select<Post> select = em.select().columns("_id", "name"). where("blog_id", Is.EQUAL, 10);
// 2Where haveCoordinaltes = new Where("latitude", Is.NOT_EQUAL, 0).or("longitude", Is.NOT_EQUAL, 0);
em.select().where("country", Is.EQUAL, "us").where( haveCoordinates);
// 3Cursor cursor = em.select().where("author", Is.LIKE, "%%alex%%"). execute();
Select:
DB Contractinterface DB {
interface Table {
}
interface Column {
String ID = BaseColumns._ID; }}
@Table(name = DB.Table.BLOGS)class Blog extends Entity {
@Column(name = DB.Column.NAME, unique = true) String name;}
import static org.exapmpe.app.DB.Column.*;
em.select().columns(ID, NAME).where(BLOG_ID, Is.EQUAL, 10);
DBOpenHelper
package org.droidparts.persist.sql;
class AbstractDBOpenHelper extends SQLiteOpenHelper { }
class DBOpenHelper extends AbstractDBOpenHelper {
@Override protected void onCreateTables(SQLiteDatabase db) { createTables(db, Blog.class, Post.class); createIndex(db, "blogs", false, "title"); }
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { addMissingColumns(db, Blog.class, Post.class) }}
EntityCursorAdapter
class PostListAdapter extends EntityCursorAdapter<Post> {
PostListAdapter(Context ctx, Select<Post> select) { super(ctx, Post.class, select); }
@Override void bindView(Context context, View view, Post item) { //TODO }}
Select object:● Has type information.● Can read an instance.● Wraps a Cursor.● Perfect data source for an Adapter.
ViewHolder
class ViewHolder {
public ViewHolder(View view) { Injector.inject(view, this); }}
View view = layoutInflater.inflate(android.R.layout. simple_list_item_1);
Text1Holder holder = new Text1Holder(view);view.setTag(holder);
holder.text1.setText("Text 1");
class Text1Holder extends ViewHolder {
@InjectView(id = android.R.id.text1) public TextView text1;
public Text1Holder(View view) { super(view); }}
JSON, ORM
Supported types:● Primitives, wrapper classes.● Enums● JSONObject, JSONArray● Uri● UUID● Drawables● Models (JSON only), Entities● Arrays & collections
JSON, ORM
public class MyClassConverter extends Converter<MyClass> {
@Override public boolean canHandle(Class<?> cls) { // TODO }
@Override public String getDBColumnType() { // TODO }
@Override public <V> MyClass readFromJSON(Class<MyClass> valType, Class<V> componentType, JSONObject obj,
String key) throws JSONException { // TODO }
@Override public <V> void putToContentValues(Class<MyClass> valueType, Class<V> componentType, ContentValues cv, String key, MyClass val) { // TODO }
// ...}
ConverterRegistry.registerConverter(new MyClassConverter());
Adding new type support:
Background Work
AsyncTask:● Ad-hoc tasks, based on user input.● Important if succeeded.● Independent, non-cancellable.● Submitting a post.
IntentService:● Regular tasks, scheduled.● Not important if succeeded.● Sequential, cancellable.● Querying for new comments.
AsyncTask
class AsyncTask<Params, Progress, Result> extends android.os.AsyncTask<...> {
abstract Result onExecute(Params... params) throws Exception;}
interface AsyncTaskResultListener<Result> {
void onAsyncTaskSuccess(Result result); void onAsyncTaskFailure(Exception e);}
AsyncTaskListener:
AsyncTask:
SimpleAsyncTask:class SimpleAsyncTask<Result> extends AsyncTask<Void, Integer, Result> {
abstract Result onExecute() throws Exception;}
IntentService
abstract Bundle onExecute(String action, Bundle data)throws Exception;
static Intent getIntent(Context ctx, Class<? extends IntentService> cls, String action)
static Intent getIntent(Context ctx, Class<? extends IntentService> cls, String action, ResultReceiver resultReceiver)
public static final int RESULT_SUCCESS = Activity.RESULT_OK;public static final int RESULT_FAILURE = Activity. RESULT_CANCELED;//public static final String EXTRA_ACTION = "__action__";public static final String EXTRA_EXCEPTION = "__exception__";
void removePendingIntents()
class IntentService extends android.app.IntentService {}
Networking Layer
RESTClient
ImageFetcher
RESTClient
● GET, PUT, POST, DELETE● Uses gzip|deflate compression.● Supports in and out headers.● Transparent cookies support.● Http basic auth.● ETag & If-Modified-Since support.● Getting response as String or InputStream.● POST multipart file.● HttpURLConnection, Apache HTTP Client,
OkHttp workers.
RestClientRESTClient(Context ctx)RESTClient(Context ctx, String userAgent)RESTClient(Context ctx, HTTPWorker worker)
RESTClient client = new RESTClient(ctx);client.authenticateBasic("user", "pass");client.setHeader("X-Header", "Val");
try { HTTPResponse resp = client.get("http://example. com/endpoint"); client.post("http://example.com/endpoint", "text/plain", "txt");} catch (HTTPException e) { // TODO }
class HTTPResponse {
public int code; public Map<String, List<String>> headers;
public String body; // or public HTTPInputStream inputStream;}
RestClient2
try { RESTClient2 client = new RESTClient2(ctx); JSONObject obj = client.getJSONObject("http://example. com/endpoint"); JSONArray arr; client.put("http://example.com/endpoint", arr);} catch (HTTPException e) { // TODO }
Adds JSON support.
class HTTPException extends Exception {
public HTTPException(int respCode, String respBody) { super(respBody); this.respCode = respCode; }
public int getResponseCode() { return respCode; }}
Data + Network Layers
JSON ORM
IntentService AsyncTask
RESTClient
ImageFetcher
class ImageFetcher {
ImageFetcher(Context ctx) { this(ctx,
new BackgroundThreadExecutor(2, "ImageFetcher-Fetch"), new RESTClient(ctx),
BitmapMemoryCache.getDefaultInstance(ctx), BitmapDiskCache.getDefaultInstance(ctx)); }}
void attachImage(String imgUrl, ImageView imageView)
// ...
void attachImage(String imgUrl, ImageView imageView, ImageReshaper reshaper, int crossFadeMillis, ImageFetchListener listener, Bitmap inBitmap)
ImageFetcher
interface ImageReshaper {
String getCacheId();
Pair<CompressFormat, Integer> getCacheFormat(String mimeType);
Bitmap.Config getBitmapConfig();
int getImageWidthHint();
int getImageHeightHint();
Bitmap reshape(Bitmap bm);
}
abstract class AbstractImageReshaper implements ImageReshaper { // TODO Check it out.}
ImageFetcher
interface ImageFetchListener {
void onFetchAdded(ImageView imageView, String imgUrl);
void onFetchProgressChanged(ImageView imageView, String imgUrl, int kBTotal, int kBReceived);
void onFetchFailed(ImageView imageView, String imgUrl, Exception e);
void onFetchCompleted(ImageView imageView, String imgUrl, Bitmap bm);
}
L(ogger)
AndroidManifest.xml:<meta-data android:name="droidparts_log_level" android:value="warn" />
Object anyObj;L.w(anyObj);
long time = System.currentTimeMillis() - start;L.wtf("Took %dms.", time);
DependencyProvider.<init>():39: Took 10ms.
DependencyProvider: Took 10ms.
Development mode:
Signed app:
See in action inDroidPartsGram
droidparts-samples/DroidPartsGram