Post on 22-Jan-2018
transcript
Painless Persistence with RealmFoo Café StockholmChristian Melchior @chrmelchior
cm@realm.io
Design for offline• A better USER EXPERIENCE!
• You always have something to show to the user.
• Reduce network requests and data transferred.
• Saves battery.
• It is 2016.
cm@realm.io
cm@realm.io
Offline architecture?MVVM
MVC
Flux
Clean Architecture
?
?? ?
??
?
?
??
?? ??
??
MVPVIPER
cm@realm.io
They all have a modelMVVM
MVC
Flux
Clean Architecture
MVPVIPER
ModelView
getData()
data
cm@realm.io
You’re doing it wrong@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Setup initial views setContentView(R.layout.activity_main);
// Load data from the REST API and show it myApi = retrofit.create(NYTimesService.class); myApi.topStories("home", "my-key") .subscribe(new Action1<List<NYTimesStory>>() { @Override public void call(List<NYTimesStory> response) { showList(response); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { showError(throwable); } });}
cm@realm.io
You’re doing it right@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Setup initial views setContentView(R.layout.activity_main);
// Load data from the Model and show it model = ((MyApplication) getApplicationContext()).getModel(); model.getTopStories() .subscribe(new Action1<List<NYTimesStory>>() { @Override public void call(List<NYTimesStory> response) { showList(response); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { showError(throwable); } });}
cm@realm.io
Repository pattern
ModelView
Cache
Database
Network
Repository
Business rules
Creating/fetching data
Repository pattern• Repository only have CRUD methods.
• Create() • Read() • Update() • Delete()
• Model and repository can be tested separately.
• http://hannesdorfmann.com/android/evolution-of-the-repository-pattern
cm@realm.io
cm@realm.io
Designing for offline
Repository… DatastoregetData()
Observable<Data>()
NetworkUpdate?
Fetch
Repository… DatastoregetData()
Observable<Data>()
NetworkUpdate?
Save
Designing for offline• Encapsulate data access.
• The datastore is “Single Source of Truth”.
• Everything is asynchronous. • Observer pattern • RxJava • EventBus
• Testing becomes easier.
cm@realm.io
Why choose Realm?
cm@realm.io
• A database designed for mobile from the ground up
• NoSQL (but not schemaless)
• Objects all the way down
• Reactive
• Cross-platform
What does Realm look like?
cm@realm.io
public class Person extends RealmObject { @PrimaryKey private long id; private String name; private int age; // References private Dog dog; private RealmList<Cat> cats;
// Methods
// …}
Saving data
cm@realm.io
realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { // Create Object directly Person person = realm.createObject(Person.class); person.setId(1); person.setName("Young Person"); person.setAge(14); // Or insert a normal Java object Person p = new Person(1, "Young Person", 14); realm.insertOrUpdate(p); }});
Queries
cm@realm.io
// Synchronous queries RealmResults<Person> results = realm.where(Person.class) .between("age", 7, 9) .beginsWith("name", "Person") .isNull("dog") .findAll();
// Asynchronous queries RealmResults<Person> results = realm.where(Person.class) .equalTo("dogs.name", "Fido") .findAllAsync();results.addChangeListener(new RealmChangeListener<RealmResults<Person>>() { @Override public void onChange(RealmResults<Person> results) { showUI(results); }});
cm@realm.io
References are first class
SELECT person.name, dog.name FROM person
INNER JOIN dog ON person.dog_id = dog.id
WHERE person.name = ‘Frank’
RealmResults<Person> results; results = realm.where(Person.class) .equalTo("name", "Frank") .findAll(); String name = results.first() .getDog().getName();
cm@realm.io
References are first class• No JOIN’s. • No object-relational impedance mismatch. • ID’s not required for references. • Navigating the object graph is as fast as
following normal references.
Zero copy / Lazy loading
cm@realm.io
Person{• name=Tommy• age=8• dog={
• name=Lassie}}
Person{• name=Tommy• age=8• dog={
• name=Lassie}}
Person{• name=Tommy• age=8• dog={
• name=Lassie}}
PersonProxy{• name• age• dog
}
PersonProxy{• name• age• dog
}
Person{• name=Tommy• age=8• dog={
• name=Lassie}}
Zero copy / Lazy loading
cm@realm.io
• Realm is always Single Source of Truth • RealmResults ~= Typesafe Cursor • Memory efficient • No need for LIMIT and Load More buttons
Caveat • Consider caching values if used in a loop.
cm@realm.io
Threading modelModel • MVCC • Each thread has a consistent
view. • Thread confined objects.
API’s • Change listeners
Model • Your on your own
API’s • Loaders • ContentObservers • ContentProviders • RxJava (3rd party) • SQLBrite (3rd party)
3xR: Realm, Retrofit and RxJava
cm@realm.io
buildscript { dependencies { classpath 'io.realm:realm-gradle-plugin:2.0.2' } } dependencies { compile 'com.google.code.gson:gson:2.7' compile 'io.reactivex:rxjava:1.2.1' compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' }
apply plugin: 'realm-android'
3xR-1: Setup Retrofit
cm@realm.io
public interface NYTimesService { @GET("svc/topstories/v1/{section}.json") Observable<List<NYTimesStory>> topStories( @Path("section") String section, @Query(value = "api-key", encoded = true) String apiKey);}
Retrofit retrofit = new Retrofit.Builder() .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .baseUrl("http://api.nytimes.com/") .build();NYTimesService api = retrofit.create(NYTimesService.class);
3xR-2: Save data
cm@realm.io
// Load data from the network and insert into Realmapi.topStories("home", apiKey).asObservable().subscribe(new Action1<List<NYTimesStory>>() { @Override public void call(final List<NYTimesStory> stories) { Realm realm = Realm.getDefaultInstance(); realm.executeTransaction(r -> { r.insertOrUpdate(stories); }); realm.close(); }}, new Action1<Throwable>() { @Override public void call(Throwable throwable) { retryOrHandleError(throwable); }});
3xR-3: Listen for changes
cm@realm.io
// Realm Observables never complete, updating your UI if anything changes Realm realm = Realm.getDefaultInstance();realm.where(NYTimesStory.class) .findAllSortedAsync("published_date", Sort.DESCENDING) .asObservable() .subscribe(new Action1<RealmResults<NYTimesStory>>() { @Override public void call(RealmResults<NYTimesStory> stories) { updateUI(stories); } });
3xR: Benefits
cm@realm.io
• Offline first with very few lines of code.
• Decouple Network and UI .
• Reactive UI: No matter where the update come from, your UI will reflect it.
Realm Mobile Platform
cm@realm.io
Automatic synchronisation between devices
Synchronising changes• Server always wins
• Easy • Client not allowed to write or risk loosing changes.
• Client can win • Hard • Complexity explosion • Changes needs to be tracked and merged.
cm@realm.io
What if we removed all that?
cm@realm.io
Native Realm Object
Realm Object Server
Only Realm
Native object
JSON
Backend object
SQL
Backend object
JSON
Native object
SQLite/CoreData SQLite/CoreData
e.g. Firebase, Parse, etc.
Synchronising a local Realm
cm@realm.io
apply plugin: 'realm-android'realm { syncEnabled = true}
cm@realm.io
SyncCredentials creds = SyncCredentials.usernamePassword(username, password, createUser);SyncUser.loginAsync(creds, "https://my.server/auth", new SyncUser.Callback() { @Override public void onSuccess(SyncUser user) { openRealm(user); } @Override public void onError(ObjectServerError error) { handleError(error); }});
Synchronising a local Realm
cm@realm.io
// Opening a local RealmRealmConfiguration config = new RealmConfiguration.Builder().build();Realm realm = Realm.getInstance(config);// Opening a synchronized Realm SyncUser user = login();String url = "realm://my.server/~/default"; SyncConfiguration config = new SyncConfiguration.Builder(user, url).build();Realm realm = Realm.getInstance(config);
Synchronising a local Realm
Features
cm@realm.io
• Automatic conflict resolution using Operational Transform.
• Offline first.
• Same reactive pattern on the Client and the Server.
• Node.js API on the server side for integration with other DB’s or API’s.
Take aways
cm@realm.io
• Design for offline first - no excuse in 2016
• Repository pattern for decoupling and testability.
• Try Realm - https://realm.io/
cm@realm.io
Questions?
Christian Melchior cm@realm.io www.realm.io @chrmelchior