SlideShare a Scribd company logo
Modern Android Architecture
Eric Maxwell

Product Engineer @ Realm

Android & iOS Developer
Columbus Kotlin User Group Organizer
Architecture
MVC
MVP
MVVM
Uni-Directional
Reactive
Architecture Components
Architecture
I’m untestable
Nobody understands me!
I’m unmaintainable :-(
Architecture
I can be tested
New Developers know exactly
where all my parts are and
how to update me!!
I’m composable!
Architectures
Understand the principles and components
• MVC on Android
• MVP
• MVVM + Data Binding
• Reactive Architecture
• Android Architecture Components
• Lifecycle
• ViewModel
• LiveData
• Room
https://github.com/ericmaxwell2003/android-architecture-samples
MVC on Android
• Model
Data + State + Business logic
• View
User Interface, visual representation of the model
• Controller
Glue to coordinate interactions between the model and the view
MVC - Model
public class Board {
private Cell[][] cells = new Cell[3][3];
private Player winner;
private GameState state;
private Player currentTurn;
private enum GameState { IN_PROGRESS, FINISHED };
public Board() {...}
/**
* Restart or start a new game, will clear the board and win status
*/
public void restart() {...}
/**
* Mark the current row for the player who's current turn it is.
* Will perform no-op if the arguments are out of range or if that position is already played.
* Will also perform a no-op if the game is already over.
*
* @param row 0..2
* @param col 0..2
* @return the player that moved or null if we did not move anything.
*
*/
public Player mark( int row, int col ) {...}
public Player getWinner() {...}
private void clearCells() {...}
private void flipCurrentTurn() {...}
private boolean isValid(int row, int col ) {...}
private boolean isOutOfBounds(int idx){...}
private boolean isCellValueAlreadySet(int row, int col) {...}
private boolean isWinningMoveByPlayer(Player player, int currentRow, int currentCol) {...}
}
public class Cell {
private Player value;
public Player getValue() { return value; }
public void setValue(Player value) { this.value = value; }
}
public enum Player { X , O }
MVC - View
MVC - Controller
public class TicTacToeController extends AppCompatActivity {
private Board model;
private ViewGroup buttonGrid;
private View winnerPlayerViewGroup;
private TextView winnerPlayerLabel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tictactoe);
winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel);
...
model = new Board();
}
public void onCellClicked(View v) {
Button button = (Button) v;
String tag = button.getTag().toString();
int row = Integer.valueOf(tag.substring(0,1));
int col = Integer.valueOf(tag.substring(1,2));
Player playerThatMoved = model.mark(row, col);
if(playerThatMoved != null) {
button.setText(playerThatMoved.toString());
if (model.getWinner() != null) {
winnerPlayerLabel.setText(playerThatMoved.toString());
winnerPlayerViewGroup.setVisibility(View.VISIBLE);
}
}
}
private void reset() {
winnerPlayerViewGroup.setVisibility(View.GONE);
winnerPlayerLabel.setText("");
model.restart();
for( int i = 0; i < buttonGrid.getChildCount(); i++ ) {
((Button) buttonGrid.getChildAt(i)).setText("");
}
}
}
MVC - Testing Model
public class TicTacToeTests {
private Board board;
@Before
public void setup() {
board = new Board();
}
/**
* This test will simulate and verify x is the winner.
*
* X | X | X
* O | |
* | O |
*/
@Test
public void test3inRowAcrossTopForX() {
board.mark(0,0); // x
assertNull(board.getWinner());
board.mark(1,0); // o
assertNull(board.getWinner());
board.mark(0,1); // x
assertNull(board.getWinner());
board.mark(2,1); // o
assertNull(board.getWinner());
board.mark(0,2); // x
assertEquals(Player.X, board.getWinner());
}
/**
* This test will simulate and verify o is the winner.
*
* O | X | X
* | O |
* | X | O
*/
@Test
public void test3inRowDiagonalFromTopLeftToBottomForO() {...}
}
MVC - Testing Controller
public class TicTacToeController extends AppCompatActivity {
private Board model;
private ViewGroup buttonGrid;
private View winnerPlayerViewGroup;
private TextView winnerPlayerLabel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tictactoe);
winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel);
...
model = new Board();
}
public void onCellClicked(View v) {
Button button = (Button) v;
String tag = button.getTag().toString();
int row = Integer.valueOf(tag.substring(0,1));
int col = Integer.valueOf(tag.substring(1,2));
Player playerThatMoved = model.mark(row, col);
if(playerThatMoved != null) {
button.setText(playerThatMoved.toString());
if (model.getWinner() != null) {
winnerPlayerLabel.setText(playerThatMoved.toString());
winnerPlayerViewGroup.setVisibility(View.VISIBLE);
}
}
}
private void reset() {
winnerPlayerViewGroup.setVisibility(View.GONE);
winnerPlayerLabel.setText("");
model.restart();
for( int i = 0; i < buttonGrid.getChildCount(); i++ ) {
((Button) buttonGrid.getChildAt(i)).setText("");
}
}
}
MVC - Testing Controller
public class TicTacToeController extends AppCompatActivity {
private Board model;
private ViewGroup buttonGrid;
private View winnerPlayerViewGroup;
private TextView winnerPlayerLabel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tictactoe);
winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel);
...
model = new Board();
}
public void onCellClicked(View v) {
Button button = (Button) v;
String tag = button.getTag().toString();
int row = Integer.valueOf(tag.substring(0,1));
int col = Integer.valueOf(tag.substring(1,2));
Player playerThatMoved = model.mark(row, col);
if(playerThatMoved != null) {
button.setText(playerThatMoved.toString());
if (model.getWinner() != null) {
winnerPlayerLabel.setText(playerThatMoved.toString());
winnerPlayerViewGroup.setVisibility(View.VISIBLE);
}
}
}
private void reset() {
winnerPlayerViewGroup.setVisibility(View.GONE);
winnerPlayerLabel.setText("");
model.restart();
for( int i = 0; i < buttonGrid.getChildCount(); i++ ) {
((Button) buttonGrid.getChildAt(i)).setText("");
}
}
}
Views, Lifecycle, Interactions 

all need mocked
MVP
• Model
Data + State + Business logic
• View
User Interface, visual representation of the model
• Presenter
Glue to coordinate interactions between the model and the view

Tells the View what to do, not how to do it!
MVP - Model
public class Board {
private Cell[][] cells = new Cell[3][3];
private Player winner;
private GameState state;
private Player currentTurn;
private enum GameState { IN_PROGRESS, FINISHED };
public Board() {...}
/**
* Restart or start a new game, will clear the board and win status
*/
public void restart() {...}
/**
* Mark the current row for the player who's current turn it is.
* Will perform no-op if the arguments are out of range or if that position is already played.
* Will also perform a no-op if the game is already over.
*
* @param row 0..2
* @param col 0..2
* @return the player that moved or null if we did not move anything.
*
*/
public Player mark( int row, int col ) {...}
public Player getWinner() {...}
private void clearCells() {...}
private void flipCurrentTurn() {...}
private boolean isValid(int row, int col ) {...}
private boolean isOutOfBounds(int idx){...}
private boolean isCellValueAlreadySet(int row, int col) {...}
private boolean isWinningMoveByPlayer(Player player, int currentRow, int currentCol) {...}
}
public class Cell {
private Player value;
public Player getValue() { return value; }
public void setValue(Player value) { this.value = value; }
}
public enum Player { X , O }
MVP - View
MVP - Controller
public class TicTacToeController extends AppCompatActivity {
private Board model;
private ViewGroup buttonGrid;
private View winnerPlayerViewGroup;
private TextView winnerPlayerLabel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tictactoe);
winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel);
...
model = new Board();
}
public void onCellClicked(View v) {
Button button = (Button) v;
String tag = button.getTag().toString();
int row = Integer.valueOf(tag.substring(0,1));
int col = Integer.valueOf(tag.substring(1,2));
Player playerThatMoved = model.mark(row, col);
if(playerThatMoved != null) {
button.setText(playerThatMoved.toString());
if (model.getWinner() != null) {
winnerPlayerLabel.setText(playerThatMoved.toString());
winnerPlayerViewGroup.setVisibility(View.VISIBLE);
}
}
}
private void reset() {
winnerPlayerViewGroup.setVisibility(View.GONE);
winnerPlayerLabel.setText("");
model.restart();
for( int i = 0; i < buttonGrid.getChildCount(); i++ ) {
((Button) buttonGrid.getChildAt(i)).setText("");
}
}
}
MVP - Presenter
public class TicTacToeActivity extends AppCompatActivity implements TicTacToeView {
private static String TAG = TicTacToeActivity.class.getName();
private ViewGroup buttonGrid;
private View winnerPlayerViewGroup;
private TextView winnerPlayerLabel;
TicTacToePresenter presenter = new TicTacToePresenter(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tictactoe);
...
presenter.onCreate();
}
@Override
protected void onPause() {
super.onPause();
presenter.onPause();
}
@Override
protected void onResume() {
super.onResume();
presenter.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
presenter.onDestroy();
}
public void onCellClicked(View v) {
Button button = (Button) v;
String tag = button.getTag().toString();
int row = Integer.valueOf(tag.substring(0,1));
int col = Integer.valueOf(tag.substring(1,2));
Log.i(TAG, "Click Row: [" + row + "," + col + "]");
presenter.onButtonSelected(row, col);
}
// View Interface Implementation
public void setButtonText(int row, int col, String text) {...}
public void clearButtons() {...}
public void showWinner(String winningPlayerDisplayLabel) {...}
public void clearWinnerDisplay() {...}
}
View
public class TicTacToePresenter extends Presenter {
private TicTacToeView view;
private Board model;
public TicTacToePresenter(TicTacToeView view) {
this.view = view;
this.model = new Board();
}
@Override
public void onCreate() {
model = new Board();
}
public void onButtonSelected(int row, int col) {
Player playerThatMoved = model.mark(row, col);
if(playerThatMoved != null) {
view.setButtonText(row, col, playerThatMoved.toString());
if (model.getWinner() != null) {
view.showWinner(playerThatMoved.toString());
}
}
}
public void onResetSelected() {
view.clearWinnerDisplay();
view.clearButtons();
model.restart();
}
}
public abstract class Presenter {
public void onCreate() {}
public void onPause() {}
public void onResume() {}
public void onDestroy() {}
}
Presenter
MVP - Presenter
public class TicTacToeActivity extends AppCompatActivity implements TicTacToeView {
private static String TAG = TicTacToeActivity.class.getName();
private ViewGroup buttonGrid;
private View winnerPlayerViewGroup;
private TextView winnerPlayerLabel;
TicTacToePresenter presenter = new TicTacToePresenter(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tictactoe);
...
presenter.onCreate();
}
@Override
protected void onPause() {
super.onPause();
presenter.onPause();
}
@Override
protected void onResume() {
super.onResume();
presenter.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
presenter.onDestroy();
}
public void onCellClicked(View v) {
Button button = (Button) v;
String tag = button.getTag().toString();
int row = Integer.valueOf(tag.substring(0,1));
int col = Integer.valueOf(tag.substring(1,2));
Log.i(TAG, "Click Row: [" + row + "," + col + "]");
presenter.onButtonSelected(row, col);
}
// View Interface Implementation
public void setButtonText(int row, int col, String text) {...}
public void clearButtons() {...}
public void showWinner(String winningPlayerDisplayLabel) {...}
public void clearWinnerDisplay() {...}
}
View
public class TicTacToePresenter extends Presenter {
private TicTacToeView view;
private Board model;
public TicTacToePresenter(TicTacToeView view) {
this.view = view;
this.model = new Board();
}
@Override
public void onCreate() {
model = new Board();
}
public void onButtonSelected(int row, int col) {
Player playerThatMoved = model.mark(row, col);
if(playerThatMoved != null) {
view.setButtonText(row, col, playerThatMoved.toString());
if (model.getWinner() != null) {
view.showWinner(playerThatMoved.toString());
}
}
}
public void onResetSelected() {
view.clearWinnerDisplay();
view.clearButtons();
model.restart();
}
}
public interface TicTacToeView {
void showWinner(String winningPlayerDisplayLabel);
void clearWinnerDisplay();
void clearButtons();
void setButtonText(int row, int col, String text);
}
Presenter
MVP - Presenter
public class TicTacToeActivity extends AppCompatActivity implements TicTacToeView {
private static String TAG = TicTacToeActivity.class.getName();
private ViewGroup buttonGrid;
private View winnerPlayerViewGroup;
private TextView winnerPlayerLabel;
TicTacToePresenter presenter = new TicTacToePresenter(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tictactoe);
...
presenter.onCreate();
}
@Override
protected void onPause() {
super.onPause();
presenter.onPause();
}
@Override
protected void onResume() {
super.onResume();
presenter.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
presenter.onDestroy();
}
public void onCellClicked(View v) {
Button button = (Button) v;
String tag = button.getTag().toString();
int row = Integer.valueOf(tag.substring(0,1));
int col = Integer.valueOf(tag.substring(1,2));
Log.i(TAG, "Click Row: [" + row + "," + col + "]");
presenter.onButtonSelected(row, col);
}
// View Interface Implementation
public void setButtonText(int row, int col, String text) {...}
public void clearButtons() {...}
public void showWinner(String winningPlayerDisplayLabel) {...}
public void clearWinnerDisplay() {...}
}
View
public class TicTacToePresenter extends Presenter {
private TicTacToeView view;
private Board model;
public TicTacToePresenter(TicTacToeView view) {
this.view = view;
this.model = new Board();
}
@Override
public void onCreate() {
model = new Board();
}
public void onButtonSelected(int row, int col) {
Player playerThatMoved = model.mark(row, col);
if(playerThatMoved != null) {
view.setButtonText(row, col, playerThatMoved.toString());
if (model.getWinner() != null) {
view.showWinner(playerThatMoved.toString());
}
}
}
public void onResetSelected() {
view.clearWinnerDisplay();
view.clearButtons();
model.restart();
}
}
public abstract class Presenter {
public void onCreate() {}
public void onPause() {}
public void onResume() {}
public void onDestroy() {}
}
Presenter
MVP - Testing Presenter
@RunWith(MockitoJUnitRunner.class)
public class TicTacToePresenterTests {
private TicTacToePresenter presenter;
@Mock
private TicTacToeView view;
@Before
public void setup() { presenter = new TicTacToePresenter(view); }
/**
* This test will simulate and verify o is the winner.
*
* X | X | X
* O | |
* | O |
*/
@Test
public void test3inRowAcrossTopForX() {
clickAndAssertValueAt(0,0, "X");
verify(view, never()).showWinner(anyString());
clickAndAssertValueAt(1,0, "O");
verify(view, never()).showWinner(anyString());
clickAndAssertValueAt(0,1, "X");
verify(view, never()).showWinner(anyString());
clickAndAssertValueAt(2,1, "O");
verify(view, never()).showWinner(anyString());
clickAndAssertValueAt(0,2, "X");
verify(view).showWinner("X");
}
private void clickAndAssertValueAt(int row, int col, String expectedValue) {
presenter.onButtonSelected(row, col);
verify(view).setButtonText(row, col, expectedValue);
}
}
MVVM + Data Binding
• Model
Data + State + Business logic
• View
Binds to observable variables and actions exposed by the ViewModel
• ViewModel
Responsible for wrapping the model and preparing observable data needed by the view. It also provides
hooks for the view to pass events to the model
MVVM - Model
public class Board {
private Cell[][] cells = new Cell[3][3];
private Player winner;
private GameState state;
private Player currentTurn;
private enum GameState { IN_PROGRESS, FINISHED };
public Board() {...}
/**
* Restart or start a new game, will clear the board and win status
*/
public void restart() {...}
/**
* Mark the current row for the player who's current turn it is.
* Will perform no-op if the arguments are out of range or if that position is already played.
* Will also perform a no-op if the game is already over.
*
* @param row 0..2
* @param col 0..2
* @return the player that moved or null if we did not move anything.
*
*/
public Player mark( int row, int col ) {...}
public Player getWinner() {...}
private void clearCells() {...}
private void flipCurrentTurn() {...}
private boolean isValid(int row, int col ) {...}
private boolean isOutOfBounds(int idx){...}
private boolean isCellValueAlreadySet(int row, int col) {...}
private boolean isWinningMoveByPlayer(Player player, int currentRow, int currentCol) {...}
}
public class Cell {
private Player value;
public Player getValue() { return value; }
public void setValue(Player value) { this.value = value; }
}
public enum Player { X , O }
MVVM - View
MVVM - ViewModel
public class TicTacToeActivity extends AppCompatActivity {
TicTacToeViewModel viewModel = new TicTacToeViewModel();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TictactoeBinding binding =
DataBindingUtil.setContentView(this, R.layout.tictactoe);
binding.setViewModel(viewModel);
viewModel.onCreate();
}
@Override
protected void onPause() {
super.onPause();
viewModel.onPause();
}
@Override
protected void onResume() {
super.onResume();
viewModel.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
viewModel.onDestroy();
}
}
View
public class TicTacToeViewModel extends ViewModel {
private Board model;
public final ObservableArrayMap<String, String> cells = new ObservableArrayMap<>();
public final ObservableField<String> winner = new ObservableField<>();
public TicTacToeViewModel() {
model = new Board();
}
public void onResetSelected() {
model.restart();
winner.set(null);
cells.clear();
}
public void onClickedCellAt(int row, int col) {
Player playerThatMoved = model.mark(row, col);
cells.put(…, …);
winner.set(…);
}
}
ViewModel
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable name="viewModel" type="com.acme.tictactoe.viewmodel.TicTacToeViewModel" />
</data>
...
<Button
style="@style/tictactoebutton"
android:onClick="@{() -> viewModel.onClickedCellAt(0,0)}"
android:text='@{viewModel.cells["00"]}' />
...
</layout>
Observing value of cells at key “00”
Invoke onClickedCellAt(0,0) on click
MVVM - Testing ViewModel
public class TicTacToeViewModelTests {
private TicTacToeViewModel viewModel;
@Before
public void setup() {
viewModel = new TicTacToeViewModel();
}
private void clickAndAssertValueAt(int row, int col, String expectedValue) {
viewModel.onClickedCellAt(row, col);
assertEquals(expectedValue, viewModel.cells.get("" + row + col));
}
/**
* This test will simulate and verify x is the winner.
*
* X | X | X
* O | |
* | O |
*/
@Test
public void test3inRowAcrossTopForX() {
clickAndAssertValueAt(0,0, "X");
assertNull(viewModel.winner.get());
clickAndAssertValueAt(1,0, "O");
assertNull(viewModel.winner.get());
clickAndAssertValueAt(0,1, "X");
assertNull(viewModel.winner.get());
clickAndAssertValueAt(2,1, "O");
assertNull(viewModel.winner.get());
clickAndAssertValueAt(0,2, "X");
assertEquals("X", viewModel.winner.get());
}
}
Checkpoint
• Understand the principles and components of
• MVC
• MVP
• MVVM
• Reactive / Uni-Directional
• Google Architecture Components
Concerns w/ MV[x]
2 States to maintain
Recreation from Big Nerd Ranch
Android Programming 3rd Edition
Chapter 2 [Android and MVC pg 38],
Controller littered with code to handle this 

(across threads)
Reactive Architecture
https://en.wikipedia.org/wiki/Model-view-controller
Uni Directional
Examples
• Flux/Redux
• Model View Intent
• Realm
• Android Architecture Components
• Uni-Directional Framework Post covering first 3

http://bit.ly/2temW62
Characteristics
• Single State
• Data and interactions flow in 1 direction
• Reactive
• Asynchronous by nature
Reactive Architecture
https://en.wikipedia.org/wiki/Model-view-controller
Uni Directional
Examples
• Flux/Redux
• Model View Intent
• Realm
• Android Architecture Components
• Uni-Directional Framework Post covering first 3

http://bit.ly/2temW62
Characteristics
• Single State
• Data and interactions flow in 1 direction
• Reactive
• Asynchronous by nature
em@realm.io
https://developer.android.com/topic/libraries/architecture/index.html
Room
ViewModel
Lifecycle
LiveData
em@realm.io
Android Architecture Components
Modern Reactive Architecture
Room
ViewModel
Lifecycle
LiveData
em@realm.io
Android Architecture Components
Modern Reactive Architecture
Lifecycle
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
public void onCreate(...) {
myLocationListener = new
MyLocationListener(this, (Message location) -> {
// update UI
});
}
public void onStart() {
super.onStart();
myLocationListener.start();
}
public void onStop() {
super.onStop();
myLocationListener.stop();
}
}
class MyLocationListener {
public MyLocationListener(
Context context, Callback callback) {
// ...
}
void start() {
// connect to system location service
}
void stop() {
// disconnect from system location service
}
}
Activity managing all the components
Lifecycle
Owner Component
Lifecycle Aware Components
Lifecycle
Owner
Component is aware of owners lifecycle
class CustomResultUserActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
public void onCreate(...) {
myLocationListener = new MyLocationListener(
this, getLifecycle(), location -> {
// update UI
});
Util.checkUserStatus(result -> {
if (result) {
myLocationListener.enable();
}
});
}
}
Lifecycle
Component is aware of owners lifecycle
class MyLocationListener implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
private void start() {
if (enabled) {
// connect
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
private void stop() {
/* disconnect if connected */
}
public void enable() {
if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
/* connect if not connected */
}
}
Component
Lifecycle
Lifecycle Aware Components
Room
ViewModel
Lifecycle
LiveData
em@realm.io
Android Architecture Components
Modern Reactive Architecture
ViewModel
em@realm.io
Configuration Changes Destroys Activities
ViewModel
em@realm.io
ViewModels
Survive Rotation
ViewModel
em@realm.iohttps://developer.android.com/topic/libraries/architecture/viewmodel.html#the_lifecycle_of_a_viewmodel
ViewModel
em@realm.io
public class MyActivity extends LifecycleActivity {
private MyViewModel mViewModel;
private TextView meTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_activity);
meTextView = (TextView) findViewById(R.id.books_tv);
// Android will instantiate my ViewModel, and the best part is
// the viewModel will survive configurationChanges!
mViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
}
}
https://developer.android.com/topic/libraries/architecture/viewmodel.html#the_lifecycle_of_a_viewmodel
ViewModel
em@realm.io
public class MyActivity extends AppCompatActivity {
private MyViewModel mViewModel;
private TextView meTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_activity);
meTextView = (TextView) findViewById(R.id.books_tv);
// Android will instantiate my ViewModel, and the best part is
// the viewModel will survive configurationChanges!
mViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
}
}
https://developer.android.com/topic/libraries/architecture/viewmodel.html#the_lifecycle_of_a_viewmodel
ViewModel
em@realm.io
public class MyViewModel extends ViewModel {
private Realm mDb;
public MyViewModel() {
// Initialization in construction
}
@Override
protected void onCleared() {
mDb.close();
}
}
public class MyViewModel extends AndroidViewModel {
private Realm mDb;
public MyViewModel(Application application) {
super(application);
}
@Override
protected void onCleared() {
mDb.close();
getApplication();
// Do something with the application
}
}
ViewModel Android ViewModel
Room
ViewModel
Lifecycle
LiveData
em@realm.io
Android Architecture Components
Modern Reactive Architecture
LiveData
em@realm.io
• An observable value container
• Lifecycle Aware
• Any data can be represented as LiveData
LiveData
em@realm.io
• LiveData<String>
• LiveData<List<Dog>>
• CustomLiveData<Dog>
LiveData Transformations
em@realm.io
LiveData<List<LoanWithUserAndBook>> loanLiveData = mDb.loanModel().findLoansByNameAfter("Mike", getYesterdayDate());
// Instead of exposing the list of Loans, we can apply a transformation and expose Strings.
LiveData<String> stringLiveData = Transformations.map(loanLiveData, new Function<List<LoanWithUserAndBook>, String>() {
@Override
public String apply(List<LoanWithUserAndBook> loansWithUserAndBook) {
StringBuilder sb = new StringBuilder();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm",
Locale.US);
for (LoanWithUserAndBook loan : loansWithUserAndBook) {
sb.append(String.format("%sn (Returned: %s)n",
loan.bookTitle,
simpleDateFormat.format(loan.endTime)));
}
return sb.toString();
}
});
Map
LiveData Transformations
em@realm.io
LiveData<List<LoanWithUserAndBook>> loanLiveData = mDb.loanModel().findLoansByNameAfter("Mike", getYesterdayDate());
// Instead of exposing the list of Loans, we can apply a transformation and expose Strings.
LiveData<String> stringLiveData = Transformations.map(loanLiveData, new Function<List<LoanWithUserAndBook>, String>() {
@Override
public String apply(List<LoanWithUserAndBook> loansWithUserAndBook) {
StringBuilder sb = new StringBuilder();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm",
Locale.US);
for (LoanWithUserAndBook loan : loansWithUserAndBook) {
sb.append(String.format("%sn (Returned: %s)n",
loan.bookTitle,
simpleDateFormat.format(loan.endTime)));
}
return sb.toString();
}
});
Map
LiveData Transformations
em@realm.io
LiveData<List<LoanWithUserAndBook>> loanLiveData = mDb.loanModel().findLoansByNameAfter("Mike", getYesterdayDate());
// Instead of exposing the list of Loans, we can apply a transformation and expose Strings.
LiveData<String> stringLiveData = Transformations.map(loanLiveData, new Function<List<LoanWithUserAndBook>, String>() {
@Override
public String apply(List<LoanWithUserAndBook> loansWithUserAndBook) {
StringBuilder sb = new StringBuilder();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm",
Locale.US);
for (LoanWithUserAndBook loan : loansWithUserAndBook) {
sb.append(String.format("%sn (Returned: %s)n",
loan.bookTitle,
simpleDateFormat.format(loan.endTime)));
}
return sb.toString();
}
});
Map
LiveData Transformations
em@realm.io
LiveData<List<LoanWithUserAndBook>> loanLiveData = mDb.loanModel().findLoansByNameAfter("Mike", getYesterdayDate());
// Instead of exposing the list of Loans, we can apply a transformation and expose Strings.
LiveData<String> stringLiveData = Transformations.map(loanLiveData, new Function<List<LoanWithUserAndBook>, String>() {
@Override
public String apply(List<LoanWithUserAndBook> loansWithUserAndBook) {
StringBuilder sb = new StringBuilder();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm",
Locale.US);
for (LoanWithUserAndBook loan : loansWithUserAndBook) {
sb.append(String.format("%sn (Returned: %s)n",
loan.bookTitle,
simpleDateFormat.format(loan.endTime)));
}
return sb.toString();
}
});
Map
LiveData Transformations
em@realm.io
LiveData<List<LoanWithUserAndBook>> loanLiveData = mDb.loanModel().findLoansByNameAfter("Mike", getYesterdayDate());
// Instead of exposing the list of Loans, we can apply a transformation and expose Strings.
LiveData<String> stringLiveData = Transformations.map(loanLiveData, new Function<List<LoanWithUserAndBook>, String>() {
@Override
public String apply(List<LoanWithUserAndBook> loansWithUserAndBook) {
StringBuilder sb = new StringBuilder();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm",
Locale.US);
for (LoanWithUserAndBook loan : loansWithUserAndBook) {
sb.append(String.format("%sn (Returned: %s)n",
loan.bookTitle,
simpleDateFormat.format(loan.endTime)));
}
return sb.toString();
}
});
Map
LiveData
em@realm.io
Subscribing to LiveData
// Here an Activity, Subscribed to LiveData

public class CustomResultUserActivity extends AppCompatActivity {
private CustomResultViewModel mShowUserViewModel;
private TextView mBooksTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.db_activity);
mBooksTextView = (TextView) findViewById(R.id.books_tv);
mShowUserViewModel = ViewModelProviders.of(this).get(CustomResultViewModel.class);
mShowUserViewModel.getLoansResult().observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable final String result) {
mBooksTextView.setText(result);
}
});
}
}
LiveData
em@realm.io
Subscribing to LiveData
// Here an Activity, Subscribed to LiveData

public class CustomResultUserActivity extends AppCompatActivity {
private CustomResultViewModel mShowUserViewModel;
private TextView mBooksTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.db_activity);
mBooksTextView = (TextView) findViewById(R.id.books_tv);
mShowUserViewModel = ViewModelProviders.of(this).get(CustomResultViewModel.class);
mShowUserViewModel.getLoansResult().observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable final String result) {
mBooksTextView.setText(result);
}
});
}
}
LiveData respects
lifecycle of
LiveData
em@realm.io
public class RealmLiveData<T extends RealmModel> extends LiveData<RealmResults<T>> {
private RealmResults<T> mResults;
private RealmChangeListener<RealmResults<T>> listener = new RealmChangeListener<RealmResults<T>>() {
@Override
public void onChange(RealmResults<T> results) {
setValue(results);
}
};
public RealmLiveData(RealmResults<T> mResults) {
this.mResults = mResults;
}
@Override
protected void onActive() {
mResults.addChangeListener(listener);
}
@Override
protected void onInactive() {
mResults.removeChangeListener(listener);
}
}
Extensible - Represent anything as LiveData
LiveData
em@realm.io
public class RealmLiveData<T extends RealmModel> extends LiveData<RealmResults<T>> {
private RealmResults<T> mResults;
private RealmChangeListener<RealmResults<T>> listener = new RealmChangeListener<RealmResults<T>>() {
@Override
public void onChange(RealmResults<T> results) {
setValue(results);
}
};
public RealmLiveData(RealmResults<T> mResults) {
this.mResults = mResults;
}
@Override
protected void onActive() {
mResults.addChangeListener(listener);
}
@Override
protected void onInactive() {
mResults.removeChangeListener(listener);
}
}
LiveData
em@realm.io
public class RealmLiveData<T extends RealmModel> extends LiveData<RealmResults<T>> {
private RealmResults<T> mResults;
private RealmChangeListener<RealmResults<T>> listener = new RealmChangeListener<RealmResults<T>>() {
@Override
public void onChange(RealmResults<T> results) {
setValue(results);
}
};
public RealmLiveData(RealmResults<T> mResults) {
this.mResults = mResults;
}
@Override
protected void onActive() {
mResults.addChangeListener(listener);
}
@Override
protected void onInactive() {
mResults.removeChangeListener(listener);
}
}
LiveData
em@realm.io
// Use Custom LiveData

public RealmLiveData<Loan> findLoansByNameAfter(final String userName, final Date after) {
return new RealmLiveData<>(mRealm.where(Loan.class)
.like("user.name", userName)
.greaterThan("endTime", after)
.findAllAsync());
}
// Can even transform that live data
RealmLiveData<Loan> loans = findLoansByNameAfter(“Mike”, getYesterdayDate());
LiveData<String> mLoansResult = Transformations.map(loans, new Function<RealmResults<Loan>, String>() {
@Override
public String apply(RealmResults<Loan> loans) {
StringBuilder sb = new StringBuilder();
for (Loan loan : loans) {
sb.append(String.format("%sn (Returned: %s)n",
loan.getBook().getTitle(),
simpleDateFormat.format(loan.getEndTime())));
}
return sb.toString();
}
});
LiveData
em@realm.io
// Use Custom LiveData

public RealmLiveData<Loan> findLoansByNameAfter(final String userName, final Date after) {
return new RealmLiveData<>(mRealm.where(Loan.class)
.like("user.name", userName)
.greaterThan("endTime", after)
.findAllAsync());
}
// Can even transform that live data
RealmLiveData<Loan> loans = findLoansByNameAfter(“Mike”, getYesterdayDate());
LiveData<String> mLoansResult = Transformations.map(loans, new Function<RealmResults<Loan>, String>() {
@Override
public String apply(RealmResults<Loan> loans) {
StringBuilder sb = new StringBuilder();
for (Loan loan : loans) {
sb.append(String.format("%sn (Returned: %s)n",
loan.getBook().getTitle(),
simpleDateFormat.format(loan.getEndTime())));
}
return sb.toString();
}
});
LiveData
em@realm.io
// Use Custom LiveData

public RealmLiveData<Loan> findLoansByNameAfter(final String userName, final Date after) {
return new RealmLiveData<>(mRealm.where(Loan.class)
.like("user.name", userName)
.greaterThan("endTime", after)
.findAllAsync());
}
// Can even transform that live data
RealmLiveData<Loan> loans = findLoansByNameAfter(“Mike”, getYesterdayDate());
LiveData<String> mLoansResult = Transformations.map(loans, new Function<RealmResults<Loan>, String>() {
@Override
public String apply(RealmResults<Loan> loans) {
StringBuilder sb = new StringBuilder();
for (Loan loan : loans) {
sb.append(String.format("%sn (Returned: %s)n",
loan.getBook().getTitle(),
simpleDateFormat.format(loan.getEndTime())));
}
return sb.toString();
}
});
Room
ViewModel
Lifecycle
LiveData
em@realm.io
Android Architecture Components
Modern Reactive Architecture
em@realm.io
Room
• ORM for SQLite
• DAOs defined in interface are generated at compile time
• Results are Live(Data)
em@realm.io
Room
em@realm.io
Room
public class LoanWithUserAndBook {
public String id;
@ColumnInfo(name="title")
public String bookTitle;
@ColumnInfo(name="name")
public String userName;
@TypeConverters(DateConverter.class)
public Date startTime;
@TypeConverters(DateConverter.class)
public Date endTime;
}
em@realm.io
Room
@Entity(foreignKeys = {
@ForeignKey(entity = Book.class,
parentColumns = "id",
childColumns = "book_id"),
@ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_id")})
@TypeConverters(DateConverter.class)
public class Loan {
public @PrimaryKey String id;
public Date startTime;
public Date endTime;
@ColumnInfo(name="book_id")
public String bookId;
@ColumnInfo(name="user_id")
public String userId;
}
Define Entites
@Entity
public class User {
public @PrimaryKey String id;
public String name;
public String lastName;
public int age;
}
@Entity
public class Book {
public @PrimaryKey String id;
public String title;
}
em@realm.io
Room
@Database(entities = {User.class, Book.class, Loan.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
private static AppDatabase INSTANCE;
public abstract UserDao userModel();
public abstract BookDao bookModel();
public abstract LoanDao loanModel();
public static AppDatabase getInMemoryDatabase(Context context) {
if (INSTANCE == null) {
INSTANCE =
Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, "LibraryDb")
.build();
}
return INSTANCE;
}
public static void destroyInstance() {
INSTANCE = null;
}
}
Define Database
em@realm.io
Room
@Database(entities = {User.class, Book.class, Loan.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
private static AppDatabase INSTANCE;
public abstract UserDao userModel();
public abstract BookDao bookModel();
public abstract LoanDao loanModel();
public static AppDatabase getInMemoryDatabase(Context context) {
if (INSTANCE == null) {
INSTANCE =
Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, "LibraryDb")
.build();
}
return INSTANCE;
}
public static void destroyInstance() {
INSTANCE = null;
}
}
Define Database
em@realm.io
Room
@Dao
@TypeConverters(DateConverter.class)
public interface LoanDao {
@Query("SELECT * From Loan")
LiveData<List<Loan>> findAll();
@Query("SELECT Loan.id, Book.title, User.name, Loan.startTime, Loan.endTime From Loan " +
"INNER JOIN Book ON Loan.book_id = Book.id " +
"INNER JOIN User ON Loan.user_id = User.id ")
LiveData<List<LoanWithUserAndBook>> findAllWithUserAndBook();
@Query("SELECT Loan.id, Book.title as title, User.name as name, Loan.startTime, Loan.endTime " +
"FROM Book " +
"INNER JOIN Loan ON Loan.book_id = Book.id " +
"INNER JOIN User on User.id = Loan.user_id " +
"WHERE User.name LIKE :userName " +
"AND Loan.endTime > :after "
)
public LiveData<List<LoanWithUserAndBook>> findLoansByNameAfter(String userName, Date after);
@Insert(onConflict = ABORT)
void insertLoan(Loan loan);
@Query("DELETE FROM Loan")
void deleteAll();
}
Define DAO Interface
em@realm.io
Room
public class CustomResultViewModel extends AndroidViewModel {
private AppDatabase mDb;
public CustomResultViewModel(Application application) {
super(application);
mDb = AppDatabase.getDatabase(application);
}
public logMikesLoans() {
LiveData<List<LoanWithUserAndBook>> loans =
mDb.loanModel().findLoansByNameAfter("Mike", getYesterdayDate());
...
}
Using Room DAOs
em@realm.io
Room
@RunWith(AndroidJUnit4.class)
public class SimpleEntityReadWriteTest {
private UserDao mUserDao;
private TestDatabase mDb;
@Before
public void createDb() {
Context context = InstrumentationRegistry.getTargetContext();
mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
mUserDao = mDb.getUserDao();
}
@After
public void closeDb() throws IOException {
mDb.close();
}
@Test
public void writeUserAndReadInList() throws Exception {
User user = TestUtil.createUser(3);
user.setName("george");
mUserDao.insert(user);
List<User> byName = mUserDao.findUsersByName("george");
assertThat(byName.get(0), equalTo(user));
}
}
Room Test Support
https://developer.android.com/topic/libraries/architecture/room.html#testing-android
em@realm.io
Android Architecture Components
In a Nutshell
Architecture
“It is impossible to have one way of writing apps that will be the
best for every scenario.
If you already have a good way of writing Android apps, you don't
need to change.
That being said, this recommended architecture should be a good
starting point for most use cases.”
https://developer.android.com/topic/libraries/architecture/guide.html#recommended_app_architecture
Official Android Architecture Dogma Guidance
For more information see: https://developer.android.com/topic/libraries/architecture/guide.html
General Good Practices
• Avoid storing state or data in Activities, Fragments, Services, Broadcast Receivers
• Keep a strong separation of concerns
• Build your Data, Model, ViewModel, Activity/Fragment, etc. as components with strong inputs / outputs
• Android Architecture Components and libraries. Focus on what makes your app unique
• Build your apps UI/UX to function well when offline
• Use Service Locators or Dependency Injection for dependencies
Additional Materials
• Android Architecture Components
• https://developer.android.com/topic/libraries/architecture/index.html
• https://github.com/googlesamples/android-architecture-components
• https://github.com/googlesamples/android-architecture (Architecture Blueprints)
• Other Reactive Uni-Directional Architectures
• Model View Intent - http://hannesdorfmann.com/android/mosby3-mvi-2#model-view-intent-mvi
• FLUX on Android - https://github.com/lgvalle/android-flux-todo-app
• Other Articles I’ve published on these topics
• https://news.realm.io/news/eric-maxwell-mvc-mvp-and-mvvm-on-android/
• https://news.realm.io/news/eric-maxwell-uni-directional-architecture-android-using-realm
• https://news.realm.io/news/android-architecture-components-and-realm/
• Samples
• https://github.com/ericmaxwell2003/android-architecture-samples
Eric Maxwell
em@realm.io
www.realm.io
@emmax
Thank you!

More Related Content

PDF
[Outdated] Secrets of Performance Tuning Java on Kubernetes
PDF
Building a fully managed stream processing platform on Flink at scale for Lin...
PDF
Running Kafka On Kubernetes With Strimzi For Real-Time Streaming Applications
PDF
0-60: Tesla's Streaming Data Platform ( Jesse Yates, Tesla) Kafka Summit SF 2019
PDF
Scaling into Billions of Nodes and Relationships with Neo4j Graph Data Science
PPTX
Migration d'une Architecture Microservice vers une Architecture Event-Driven ...
PPTX
Tuning Apache Kafka Connectors for Flink.pptx
PDF
Introducing the Apache Flink Kubernetes Operator
[Outdated] Secrets of Performance Tuning Java on Kubernetes
Building a fully managed stream processing platform on Flink at scale for Lin...
Running Kafka On Kubernetes With Strimzi For Real-Time Streaming Applications
0-60: Tesla's Streaming Data Platform ( Jesse Yates, Tesla) Kafka Summit SF 2019
Scaling into Billions of Nodes and Relationships with Neo4j Graph Data Science
Migration d'une Architecture Microservice vers une Architecture Event-Driven ...
Tuning Apache Kafka Connectors for Flink.pptx
Introducing the Apache Flink Kubernetes Operator

What's hot (20)

PDF
Mind the App: How to Monitor Your Kafka Streams Applications | Bruno Cadonna,...
PDF
Pinot: Realtime OLAP for 530 Million Users - Sigmod 2018
PDF
Neo4j Graph Data Science Training - June 9 & 10 - Slides #5 - Graph Catalog O...
PDF
Performance Tuning RocksDB for Kafka Streams' State Stores (Dhruba Borthakur,...
PPTX
Spark + Cassandra = Real Time Analytics on Operational Data
PDF
Apache Kafka Architecture & Fundamentals Explained
PDF
RAC Troubleshooting and Diagnosability Sangam2016
PDF
Performance Tuning RocksDB for Kafka Streams’ State Stores
PPTX
Event-Based API Patterns and Practices
PDF
Apples and Oranges - Comparing Kafka Streams and Flink with Bill Bejeck
PDF
Introduction to Apache Flink - Fast and reliable big data processing
PDF
Kafka tiered-storage-meetup-2022-final-presented
PPTX
Squirreling Away $640 Billion: How Stripe Leverages Flink for Change Data Cap...
PPTX
Apache kafka
PDF
Machine Learning with PyCarent + MLflow
PDF
Fluentd vs. Logstash for OpenStack Log Management
PDF
Stream Processing with Apache Kafka and .NET
PDF
hubot-slack v4移行時のハマりどころ #hubot_chatops
PPTX
The Current State of Table API in 2022
PDF
How Kafka Powers the World's Most Popular Vector Database System with Charles...
Mind the App: How to Monitor Your Kafka Streams Applications | Bruno Cadonna,...
Pinot: Realtime OLAP for 530 Million Users - Sigmod 2018
Neo4j Graph Data Science Training - June 9 & 10 - Slides #5 - Graph Catalog O...
Performance Tuning RocksDB for Kafka Streams' State Stores (Dhruba Borthakur,...
Spark + Cassandra = Real Time Analytics on Operational Data
Apache Kafka Architecture & Fundamentals Explained
RAC Troubleshooting and Diagnosability Sangam2016
Performance Tuning RocksDB for Kafka Streams’ State Stores
Event-Based API Patterns and Practices
Apples and Oranges - Comparing Kafka Streams and Flink with Bill Bejeck
Introduction to Apache Flink - Fast and reliable big data processing
Kafka tiered-storage-meetup-2022-final-presented
Squirreling Away $640 Billion: How Stripe Leverages Flink for Change Data Cap...
Apache kafka
Machine Learning with PyCarent + MLflow
Fluentd vs. Logstash for OpenStack Log Management
Stream Processing with Apache Kafka and .NET
hubot-slack v4移行時のハマりどころ #hubot_chatops
The Current State of Table API in 2022
How Kafka Powers the World's Most Popular Vector Database System with Charles...
Ad

Similar to Modern Android Architecture (20)

PDF
Working effectively with ViewModels and TDD - UA Mobile 2019
PPTX
Presentation Android Architecture Components
PPTX
Net conf BG xamarin lecture
PDF
Does testability imply good design - Andrzej Jóźwiak - TomTom Dev Day 2022
PDF
[22]Efficient and Testable MVVM pattern
PPTX
Tricks to Making a Realtime SurfaceView Actually Perform in Realtime - Maarte...
PDF
Griffon @ Svwjug
PDF
Useful Tools for Making Video Games - XNA (2008)
PDF
Model View Intent on Android
PPTX
Применение шаблона проектирования MVVM при разработке архитектуры Windows Pho...
PDF
준비하세요 Angular js 2.0
PDF
My way to clean android v2 English DroidCon Spain
PPTX
Dependency Injection for Android @ Ciklum speakers corner Kiev 29. May 2014
PPTX
Dependency Injection for Android
PDF
Android and the Seven Dwarfs from Devox'15
PDF
My way to clean android V2
PDF
Android Design Patterns
PDF
Synchronizing without internet - Multipeer Connectivity (iOS)
PDF
Practical Model View Programming (Roadshow Version)
PDF
Tilting at Windmills with ctypes and cygwinreg
Working effectively with ViewModels and TDD - UA Mobile 2019
Presentation Android Architecture Components
Net conf BG xamarin lecture
Does testability imply good design - Andrzej Jóźwiak - TomTom Dev Day 2022
[22]Efficient and Testable MVVM pattern
Tricks to Making a Realtime SurfaceView Actually Perform in Realtime - Maarte...
Griffon @ Svwjug
Useful Tools for Making Video Games - XNA (2008)
Model View Intent on Android
Применение шаблона проектирования MVVM при разработке архитектуры Windows Pho...
준비하세요 Angular js 2.0
My way to clean android v2 English DroidCon Spain
Dependency Injection for Android @ Ciklum speakers corner Kiev 29. May 2014
Dependency Injection for Android
Android and the Seven Dwarfs from Devox'15
My way to clean android V2
Android Design Patterns
Synchronizing without internet - Multipeer Connectivity (iOS)
Practical Model View Programming (Roadshow Version)
Tilting at Windmills with ctypes and cygwinreg
Ad

Recently uploaded (20)

PDF
System and Network Administration Chapter 2
PDF
Nekopoi APK 2025 free lastest update
PDF
Upgrade and Innovation Strategies for SAP ERP Customers
PPTX
Essential Infomation Tech presentation.pptx
PDF
Adobe Illustrator 28.6 Crack My Vision of Vector Design
PDF
Raksha Bandhan Grocery Pricing Trends in India 2025.pdf
PDF
System and Network Administraation Chapter 3
PDF
Internet Downloader Manager (IDM) Crack 6.42 Build 41
PPTX
Operating system designcfffgfgggggggvggggggggg
PDF
Wondershare Filmora 15 Crack With Activation Key [2025
PDF
EN-Survey-Report-SAP-LeanIX-EA-Insights-2025.pdf
PDF
Adobe Premiere Pro 2025 (v24.5.0.057) Crack free
PDF
SAP S4 Hana Brochure 3 (PTS SYSTEMS AND SOLUTIONS)
PPTX
CHAPTER 2 - PM Management and IT Context
PDF
Claude Code: Everyone is a 10x Developer - A Comprehensive AI-Powered CLI Tool
PDF
Design an Analysis of Algorithms II-SECS-1021-03
PDF
Audit Checklist Design Aligning with ISO, IATF, and Industry Standards — Omne...
PDF
medical staffing services at VALiNTRY
PDF
top salesforce developer skills in 2025.pdf
PDF
Internet Downloader Manager (IDM) Crack 6.42 Build 42 Updates Latest 2025
System and Network Administration Chapter 2
Nekopoi APK 2025 free lastest update
Upgrade and Innovation Strategies for SAP ERP Customers
Essential Infomation Tech presentation.pptx
Adobe Illustrator 28.6 Crack My Vision of Vector Design
Raksha Bandhan Grocery Pricing Trends in India 2025.pdf
System and Network Administraation Chapter 3
Internet Downloader Manager (IDM) Crack 6.42 Build 41
Operating system designcfffgfgggggggvggggggggg
Wondershare Filmora 15 Crack With Activation Key [2025
EN-Survey-Report-SAP-LeanIX-EA-Insights-2025.pdf
Adobe Premiere Pro 2025 (v24.5.0.057) Crack free
SAP S4 Hana Brochure 3 (PTS SYSTEMS AND SOLUTIONS)
CHAPTER 2 - PM Management and IT Context
Claude Code: Everyone is a 10x Developer - A Comprehensive AI-Powered CLI Tool
Design an Analysis of Algorithms II-SECS-1021-03
Audit Checklist Design Aligning with ISO, IATF, and Industry Standards — Omne...
medical staffing services at VALiNTRY
top salesforce developer skills in 2025.pdf
Internet Downloader Manager (IDM) Crack 6.42 Build 42 Updates Latest 2025

Modern Android Architecture

  • 1. Modern Android Architecture Eric Maxwell
 Product Engineer @ Realm
 Android & iOS Developer Columbus Kotlin User Group Organizer
  • 4. Architecture I can be tested New Developers know exactly where all my parts are and how to update me!! I’m composable!
  • 5. Architectures Understand the principles and components • MVC on Android • MVP • MVVM + Data Binding • Reactive Architecture • Android Architecture Components • Lifecycle • ViewModel • LiveData • Room https://github.com/ericmaxwell2003/android-architecture-samples
  • 6. MVC on Android • Model Data + State + Business logic • View User Interface, visual representation of the model • Controller Glue to coordinate interactions between the model and the view
  • 7. MVC - Model public class Board { private Cell[][] cells = new Cell[3][3]; private Player winner; private GameState state; private Player currentTurn; private enum GameState { IN_PROGRESS, FINISHED }; public Board() {...} /** * Restart or start a new game, will clear the board and win status */ public void restart() {...} /** * Mark the current row for the player who's current turn it is. * Will perform no-op if the arguments are out of range or if that position is already played. * Will also perform a no-op if the game is already over. * * @param row 0..2 * @param col 0..2 * @return the player that moved or null if we did not move anything. * */ public Player mark( int row, int col ) {...} public Player getWinner() {...} private void clearCells() {...} private void flipCurrentTurn() {...} private boolean isValid(int row, int col ) {...} private boolean isOutOfBounds(int idx){...} private boolean isCellValueAlreadySet(int row, int col) {...} private boolean isWinningMoveByPlayer(Player player, int currentRow, int currentCol) {...} } public class Cell { private Player value; public Player getValue() { return value; } public void setValue(Player value) { this.value = value; } } public enum Player { X , O }
  • 9. MVC - Controller public class TicTacToeController extends AppCompatActivity { private Board model; private ViewGroup buttonGrid; private View winnerPlayerViewGroup; private TextView winnerPlayerLabel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.tictactoe); winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel); ... model = new Board(); } public void onCellClicked(View v) { Button button = (Button) v; String tag = button.getTag().toString(); int row = Integer.valueOf(tag.substring(0,1)); int col = Integer.valueOf(tag.substring(1,2)); Player playerThatMoved = model.mark(row, col); if(playerThatMoved != null) { button.setText(playerThatMoved.toString()); if (model.getWinner() != null) { winnerPlayerLabel.setText(playerThatMoved.toString()); winnerPlayerViewGroup.setVisibility(View.VISIBLE); } } } private void reset() { winnerPlayerViewGroup.setVisibility(View.GONE); winnerPlayerLabel.setText(""); model.restart(); for( int i = 0; i < buttonGrid.getChildCount(); i++ ) { ((Button) buttonGrid.getChildAt(i)).setText(""); } } }
  • 10. MVC - Testing Model public class TicTacToeTests { private Board board; @Before public void setup() { board = new Board(); } /** * This test will simulate and verify x is the winner. * * X | X | X * O | | * | O | */ @Test public void test3inRowAcrossTopForX() { board.mark(0,0); // x assertNull(board.getWinner()); board.mark(1,0); // o assertNull(board.getWinner()); board.mark(0,1); // x assertNull(board.getWinner()); board.mark(2,1); // o assertNull(board.getWinner()); board.mark(0,2); // x assertEquals(Player.X, board.getWinner()); } /** * This test will simulate and verify o is the winner. * * O | X | X * | O | * | X | O */ @Test public void test3inRowDiagonalFromTopLeftToBottomForO() {...} }
  • 11. MVC - Testing Controller public class TicTacToeController extends AppCompatActivity { private Board model; private ViewGroup buttonGrid; private View winnerPlayerViewGroup; private TextView winnerPlayerLabel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.tictactoe); winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel); ... model = new Board(); } public void onCellClicked(View v) { Button button = (Button) v; String tag = button.getTag().toString(); int row = Integer.valueOf(tag.substring(0,1)); int col = Integer.valueOf(tag.substring(1,2)); Player playerThatMoved = model.mark(row, col); if(playerThatMoved != null) { button.setText(playerThatMoved.toString()); if (model.getWinner() != null) { winnerPlayerLabel.setText(playerThatMoved.toString()); winnerPlayerViewGroup.setVisibility(View.VISIBLE); } } } private void reset() { winnerPlayerViewGroup.setVisibility(View.GONE); winnerPlayerLabel.setText(""); model.restart(); for( int i = 0; i < buttonGrid.getChildCount(); i++ ) { ((Button) buttonGrid.getChildAt(i)).setText(""); } } }
  • 12. MVC - Testing Controller public class TicTacToeController extends AppCompatActivity { private Board model; private ViewGroup buttonGrid; private View winnerPlayerViewGroup; private TextView winnerPlayerLabel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.tictactoe); winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel); ... model = new Board(); } public void onCellClicked(View v) { Button button = (Button) v; String tag = button.getTag().toString(); int row = Integer.valueOf(tag.substring(0,1)); int col = Integer.valueOf(tag.substring(1,2)); Player playerThatMoved = model.mark(row, col); if(playerThatMoved != null) { button.setText(playerThatMoved.toString()); if (model.getWinner() != null) { winnerPlayerLabel.setText(playerThatMoved.toString()); winnerPlayerViewGroup.setVisibility(View.VISIBLE); } } } private void reset() { winnerPlayerViewGroup.setVisibility(View.GONE); winnerPlayerLabel.setText(""); model.restart(); for( int i = 0; i < buttonGrid.getChildCount(); i++ ) { ((Button) buttonGrid.getChildAt(i)).setText(""); } } } Views, Lifecycle, Interactions 
 all need mocked
  • 13. MVP • Model Data + State + Business logic • View User Interface, visual representation of the model • Presenter Glue to coordinate interactions between the model and the view
 Tells the View what to do, not how to do it!
  • 14. MVP - Model public class Board { private Cell[][] cells = new Cell[3][3]; private Player winner; private GameState state; private Player currentTurn; private enum GameState { IN_PROGRESS, FINISHED }; public Board() {...} /** * Restart or start a new game, will clear the board and win status */ public void restart() {...} /** * Mark the current row for the player who's current turn it is. * Will perform no-op if the arguments are out of range or if that position is already played. * Will also perform a no-op if the game is already over. * * @param row 0..2 * @param col 0..2 * @return the player that moved or null if we did not move anything. * */ public Player mark( int row, int col ) {...} public Player getWinner() {...} private void clearCells() {...} private void flipCurrentTurn() {...} private boolean isValid(int row, int col ) {...} private boolean isOutOfBounds(int idx){...} private boolean isCellValueAlreadySet(int row, int col) {...} private boolean isWinningMoveByPlayer(Player player, int currentRow, int currentCol) {...} } public class Cell { private Player value; public Player getValue() { return value; } public void setValue(Player value) { this.value = value; } } public enum Player { X , O }
  • 16. MVP - Controller public class TicTacToeController extends AppCompatActivity { private Board model; private ViewGroup buttonGrid; private View winnerPlayerViewGroup; private TextView winnerPlayerLabel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.tictactoe); winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel); ... model = new Board(); } public void onCellClicked(View v) { Button button = (Button) v; String tag = button.getTag().toString(); int row = Integer.valueOf(tag.substring(0,1)); int col = Integer.valueOf(tag.substring(1,2)); Player playerThatMoved = model.mark(row, col); if(playerThatMoved != null) { button.setText(playerThatMoved.toString()); if (model.getWinner() != null) { winnerPlayerLabel.setText(playerThatMoved.toString()); winnerPlayerViewGroup.setVisibility(View.VISIBLE); } } } private void reset() { winnerPlayerViewGroup.setVisibility(View.GONE); winnerPlayerLabel.setText(""); model.restart(); for( int i = 0; i < buttonGrid.getChildCount(); i++ ) { ((Button) buttonGrid.getChildAt(i)).setText(""); } } }
  • 17. MVP - Presenter public class TicTacToeActivity extends AppCompatActivity implements TicTacToeView { private static String TAG = TicTacToeActivity.class.getName(); private ViewGroup buttonGrid; private View winnerPlayerViewGroup; private TextView winnerPlayerLabel; TicTacToePresenter presenter = new TicTacToePresenter(this); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.tictactoe); ... presenter.onCreate(); } @Override protected void onPause() { super.onPause(); presenter.onPause(); } @Override protected void onResume() { super.onResume(); presenter.onResume(); } @Override protected void onDestroy() { super.onDestroy(); presenter.onDestroy(); } public void onCellClicked(View v) { Button button = (Button) v; String tag = button.getTag().toString(); int row = Integer.valueOf(tag.substring(0,1)); int col = Integer.valueOf(tag.substring(1,2)); Log.i(TAG, "Click Row: [" + row + "," + col + "]"); presenter.onButtonSelected(row, col); } // View Interface Implementation public void setButtonText(int row, int col, String text) {...} public void clearButtons() {...} public void showWinner(String winningPlayerDisplayLabel) {...} public void clearWinnerDisplay() {...} } View public class TicTacToePresenter extends Presenter { private TicTacToeView view; private Board model; public TicTacToePresenter(TicTacToeView view) { this.view = view; this.model = new Board(); } @Override public void onCreate() { model = new Board(); } public void onButtonSelected(int row, int col) { Player playerThatMoved = model.mark(row, col); if(playerThatMoved != null) { view.setButtonText(row, col, playerThatMoved.toString()); if (model.getWinner() != null) { view.showWinner(playerThatMoved.toString()); } } } public void onResetSelected() { view.clearWinnerDisplay(); view.clearButtons(); model.restart(); } } public abstract class Presenter { public void onCreate() {} public void onPause() {} public void onResume() {} public void onDestroy() {} } Presenter
  • 18. MVP - Presenter public class TicTacToeActivity extends AppCompatActivity implements TicTacToeView { private static String TAG = TicTacToeActivity.class.getName(); private ViewGroup buttonGrid; private View winnerPlayerViewGroup; private TextView winnerPlayerLabel; TicTacToePresenter presenter = new TicTacToePresenter(this); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.tictactoe); ... presenter.onCreate(); } @Override protected void onPause() { super.onPause(); presenter.onPause(); } @Override protected void onResume() { super.onResume(); presenter.onResume(); } @Override protected void onDestroy() { super.onDestroy(); presenter.onDestroy(); } public void onCellClicked(View v) { Button button = (Button) v; String tag = button.getTag().toString(); int row = Integer.valueOf(tag.substring(0,1)); int col = Integer.valueOf(tag.substring(1,2)); Log.i(TAG, "Click Row: [" + row + "," + col + "]"); presenter.onButtonSelected(row, col); } // View Interface Implementation public void setButtonText(int row, int col, String text) {...} public void clearButtons() {...} public void showWinner(String winningPlayerDisplayLabel) {...} public void clearWinnerDisplay() {...} } View public class TicTacToePresenter extends Presenter { private TicTacToeView view; private Board model; public TicTacToePresenter(TicTacToeView view) { this.view = view; this.model = new Board(); } @Override public void onCreate() { model = new Board(); } public void onButtonSelected(int row, int col) { Player playerThatMoved = model.mark(row, col); if(playerThatMoved != null) { view.setButtonText(row, col, playerThatMoved.toString()); if (model.getWinner() != null) { view.showWinner(playerThatMoved.toString()); } } } public void onResetSelected() { view.clearWinnerDisplay(); view.clearButtons(); model.restart(); } } public interface TicTacToeView { void showWinner(String winningPlayerDisplayLabel); void clearWinnerDisplay(); void clearButtons(); void setButtonText(int row, int col, String text); } Presenter
  • 19. MVP - Presenter public class TicTacToeActivity extends AppCompatActivity implements TicTacToeView { private static String TAG = TicTacToeActivity.class.getName(); private ViewGroup buttonGrid; private View winnerPlayerViewGroup; private TextView winnerPlayerLabel; TicTacToePresenter presenter = new TicTacToePresenter(this); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.tictactoe); ... presenter.onCreate(); } @Override protected void onPause() { super.onPause(); presenter.onPause(); } @Override protected void onResume() { super.onResume(); presenter.onResume(); } @Override protected void onDestroy() { super.onDestroy(); presenter.onDestroy(); } public void onCellClicked(View v) { Button button = (Button) v; String tag = button.getTag().toString(); int row = Integer.valueOf(tag.substring(0,1)); int col = Integer.valueOf(tag.substring(1,2)); Log.i(TAG, "Click Row: [" + row + "," + col + "]"); presenter.onButtonSelected(row, col); } // View Interface Implementation public void setButtonText(int row, int col, String text) {...} public void clearButtons() {...} public void showWinner(String winningPlayerDisplayLabel) {...} public void clearWinnerDisplay() {...} } View public class TicTacToePresenter extends Presenter { private TicTacToeView view; private Board model; public TicTacToePresenter(TicTacToeView view) { this.view = view; this.model = new Board(); } @Override public void onCreate() { model = new Board(); } public void onButtonSelected(int row, int col) { Player playerThatMoved = model.mark(row, col); if(playerThatMoved != null) { view.setButtonText(row, col, playerThatMoved.toString()); if (model.getWinner() != null) { view.showWinner(playerThatMoved.toString()); } } } public void onResetSelected() { view.clearWinnerDisplay(); view.clearButtons(); model.restart(); } } public abstract class Presenter { public void onCreate() {} public void onPause() {} public void onResume() {} public void onDestroy() {} } Presenter
  • 20. MVP - Testing Presenter @RunWith(MockitoJUnitRunner.class) public class TicTacToePresenterTests { private TicTacToePresenter presenter; @Mock private TicTacToeView view; @Before public void setup() { presenter = new TicTacToePresenter(view); } /** * This test will simulate and verify o is the winner. * * X | X | X * O | | * | O | */ @Test public void test3inRowAcrossTopForX() { clickAndAssertValueAt(0,0, "X"); verify(view, never()).showWinner(anyString()); clickAndAssertValueAt(1,0, "O"); verify(view, never()).showWinner(anyString()); clickAndAssertValueAt(0,1, "X"); verify(view, never()).showWinner(anyString()); clickAndAssertValueAt(2,1, "O"); verify(view, never()).showWinner(anyString()); clickAndAssertValueAt(0,2, "X"); verify(view).showWinner("X"); } private void clickAndAssertValueAt(int row, int col, String expectedValue) { presenter.onButtonSelected(row, col); verify(view).setButtonText(row, col, expectedValue); } }
  • 21. MVVM + Data Binding • Model Data + State + Business logic • View Binds to observable variables and actions exposed by the ViewModel • ViewModel Responsible for wrapping the model and preparing observable data needed by the view. It also provides hooks for the view to pass events to the model
  • 22. MVVM - Model public class Board { private Cell[][] cells = new Cell[3][3]; private Player winner; private GameState state; private Player currentTurn; private enum GameState { IN_PROGRESS, FINISHED }; public Board() {...} /** * Restart or start a new game, will clear the board and win status */ public void restart() {...} /** * Mark the current row for the player who's current turn it is. * Will perform no-op if the arguments are out of range or if that position is already played. * Will also perform a no-op if the game is already over. * * @param row 0..2 * @param col 0..2 * @return the player that moved or null if we did not move anything. * */ public Player mark( int row, int col ) {...} public Player getWinner() {...} private void clearCells() {...} private void flipCurrentTurn() {...} private boolean isValid(int row, int col ) {...} private boolean isOutOfBounds(int idx){...} private boolean isCellValueAlreadySet(int row, int col) {...} private boolean isWinningMoveByPlayer(Player player, int currentRow, int currentCol) {...} } public class Cell { private Player value; public Player getValue() { return value; } public void setValue(Player value) { this.value = value; } } public enum Player { X , O }
  • 24. MVVM - ViewModel public class TicTacToeActivity extends AppCompatActivity { TicTacToeViewModel viewModel = new TicTacToeViewModel(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TictactoeBinding binding = DataBindingUtil.setContentView(this, R.layout.tictactoe); binding.setViewModel(viewModel); viewModel.onCreate(); } @Override protected void onPause() { super.onPause(); viewModel.onPause(); } @Override protected void onResume() { super.onResume(); viewModel.onResume(); } @Override protected void onDestroy() { super.onDestroy(); viewModel.onDestroy(); } } View public class TicTacToeViewModel extends ViewModel { private Board model; public final ObservableArrayMap<String, String> cells = new ObservableArrayMap<>(); public final ObservableField<String> winner = new ObservableField<>(); public TicTacToeViewModel() { model = new Board(); } public void onResetSelected() { model.restart(); winner.set(null); cells.clear(); } public void onClickedCellAt(int row, int col) { Player playerThatMoved = model.mark(row, col); cells.put(…, …); winner.set(…); } } ViewModel <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View" /> <variable name="viewModel" type="com.acme.tictactoe.viewmodel.TicTacToeViewModel" /> </data> ... <Button style="@style/tictactoebutton" android:onClick="@{() -> viewModel.onClickedCellAt(0,0)}" android:text='@{viewModel.cells["00"]}' /> ... </layout> Observing value of cells at key “00” Invoke onClickedCellAt(0,0) on click
  • 25. MVVM - Testing ViewModel public class TicTacToeViewModelTests { private TicTacToeViewModel viewModel; @Before public void setup() { viewModel = new TicTacToeViewModel(); } private void clickAndAssertValueAt(int row, int col, String expectedValue) { viewModel.onClickedCellAt(row, col); assertEquals(expectedValue, viewModel.cells.get("" + row + col)); } /** * This test will simulate and verify x is the winner. * * X | X | X * O | | * | O | */ @Test public void test3inRowAcrossTopForX() { clickAndAssertValueAt(0,0, "X"); assertNull(viewModel.winner.get()); clickAndAssertValueAt(1,0, "O"); assertNull(viewModel.winner.get()); clickAndAssertValueAt(0,1, "X"); assertNull(viewModel.winner.get()); clickAndAssertValueAt(2,1, "O"); assertNull(viewModel.winner.get()); clickAndAssertValueAt(0,2, "X"); assertEquals("X", viewModel.winner.get()); } }
  • 26. Checkpoint • Understand the principles and components of • MVC • MVP • MVVM • Reactive / Uni-Directional • Google Architecture Components
  • 27. Concerns w/ MV[x] 2 States to maintain Recreation from Big Nerd Ranch Android Programming 3rd Edition Chapter 2 [Android and MVC pg 38], Controller littered with code to handle this 
 (across threads)
  • 28. Reactive Architecture https://en.wikipedia.org/wiki/Model-view-controller Uni Directional Examples • Flux/Redux • Model View Intent • Realm • Android Architecture Components • Uni-Directional Framework Post covering first 3
 http://bit.ly/2temW62 Characteristics • Single State • Data and interactions flow in 1 direction • Reactive • Asynchronous by nature
  • 29. Reactive Architecture https://en.wikipedia.org/wiki/Model-view-controller Uni Directional Examples • Flux/Redux • Model View Intent • Realm • Android Architecture Components • Uni-Directional Framework Post covering first 3
 http://bit.ly/2temW62 Characteristics • Single State • Data and interactions flow in 1 direction • Reactive • Asynchronous by nature
  • 33. Lifecycle class MyActivity extends AppCompatActivity { private MyLocationListener myLocationListener; public void onCreate(...) { myLocationListener = new MyLocationListener(this, (Message location) -> { // update UI }); } public void onStart() { super.onStart(); myLocationListener.start(); } public void onStop() { super.onStop(); myLocationListener.stop(); } } class MyLocationListener { public MyLocationListener( Context context, Callback callback) { // ... } void start() { // connect to system location service } void stop() { // disconnect from system location service } } Activity managing all the components
  • 35. Lifecycle Owner Component is aware of owners lifecycle class CustomResultUserActivity extends AppCompatActivity { private MyLocationListener myLocationListener; public void onCreate(...) { myLocationListener = new MyLocationListener( this, getLifecycle(), location -> { // update UI }); Util.checkUserStatus(result -> { if (result) { myLocationListener.enable(); } }); } }
  • 36. Lifecycle Component is aware of owners lifecycle class MyLocationListener implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_START) private void start() { if (enabled) { // connect } } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) private void stop() { /* disconnect if connected */ } public void enable() { if (lifecycle.getCurrentState().isAtLeast(STARTED)) { /* connect if not connected */ } } Component
  • 42. ViewModel em@realm.io public class MyActivity extends LifecycleActivity { private MyViewModel mViewModel; private TextView meTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.my_activity); meTextView = (TextView) findViewById(R.id.books_tv); // Android will instantiate my ViewModel, and the best part is // the viewModel will survive configurationChanges! mViewModel = ViewModelProviders.of(this).get(MyViewModel.class); } } https://developer.android.com/topic/libraries/architecture/viewmodel.html#the_lifecycle_of_a_viewmodel
  • 43. ViewModel em@realm.io public class MyActivity extends AppCompatActivity { private MyViewModel mViewModel; private TextView meTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.my_activity); meTextView = (TextView) findViewById(R.id.books_tv); // Android will instantiate my ViewModel, and the best part is // the viewModel will survive configurationChanges! mViewModel = ViewModelProviders.of(this).get(MyViewModel.class); } } https://developer.android.com/topic/libraries/architecture/viewmodel.html#the_lifecycle_of_a_viewmodel
  • 44. ViewModel em@realm.io public class MyViewModel extends ViewModel { private Realm mDb; public MyViewModel() { // Initialization in construction } @Override protected void onCleared() { mDb.close(); } } public class MyViewModel extends AndroidViewModel { private Realm mDb; public MyViewModel(Application application) { super(application); } @Override protected void onCleared() { mDb.close(); getApplication(); // Do something with the application } } ViewModel Android ViewModel
  • 46. LiveData em@realm.io • An observable value container • Lifecycle Aware • Any data can be represented as LiveData
  • 48. LiveData Transformations em@realm.io LiveData<List<LoanWithUserAndBook>> loanLiveData = mDb.loanModel().findLoansByNameAfter("Mike", getYesterdayDate()); // Instead of exposing the list of Loans, we can apply a transformation and expose Strings. LiveData<String> stringLiveData = Transformations.map(loanLiveData, new Function<List<LoanWithUserAndBook>, String>() { @Override public String apply(List<LoanWithUserAndBook> loansWithUserAndBook) { StringBuilder sb = new StringBuilder(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US); for (LoanWithUserAndBook loan : loansWithUserAndBook) { sb.append(String.format("%sn (Returned: %s)n", loan.bookTitle, simpleDateFormat.format(loan.endTime))); } return sb.toString(); } }); Map
  • 49. LiveData Transformations em@realm.io LiveData<List<LoanWithUserAndBook>> loanLiveData = mDb.loanModel().findLoansByNameAfter("Mike", getYesterdayDate()); // Instead of exposing the list of Loans, we can apply a transformation and expose Strings. LiveData<String> stringLiveData = Transformations.map(loanLiveData, new Function<List<LoanWithUserAndBook>, String>() { @Override public String apply(List<LoanWithUserAndBook> loansWithUserAndBook) { StringBuilder sb = new StringBuilder(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US); for (LoanWithUserAndBook loan : loansWithUserAndBook) { sb.append(String.format("%sn (Returned: %s)n", loan.bookTitle, simpleDateFormat.format(loan.endTime))); } return sb.toString(); } }); Map
  • 50. LiveData Transformations em@realm.io LiveData<List<LoanWithUserAndBook>> loanLiveData = mDb.loanModel().findLoansByNameAfter("Mike", getYesterdayDate()); // Instead of exposing the list of Loans, we can apply a transformation and expose Strings. LiveData<String> stringLiveData = Transformations.map(loanLiveData, new Function<List<LoanWithUserAndBook>, String>() { @Override public String apply(List<LoanWithUserAndBook> loansWithUserAndBook) { StringBuilder sb = new StringBuilder(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US); for (LoanWithUserAndBook loan : loansWithUserAndBook) { sb.append(String.format("%sn (Returned: %s)n", loan.bookTitle, simpleDateFormat.format(loan.endTime))); } return sb.toString(); } }); Map
  • 51. LiveData Transformations em@realm.io LiveData<List<LoanWithUserAndBook>> loanLiveData = mDb.loanModel().findLoansByNameAfter("Mike", getYesterdayDate()); // Instead of exposing the list of Loans, we can apply a transformation and expose Strings. LiveData<String> stringLiveData = Transformations.map(loanLiveData, new Function<List<LoanWithUserAndBook>, String>() { @Override public String apply(List<LoanWithUserAndBook> loansWithUserAndBook) { StringBuilder sb = new StringBuilder(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US); for (LoanWithUserAndBook loan : loansWithUserAndBook) { sb.append(String.format("%sn (Returned: %s)n", loan.bookTitle, simpleDateFormat.format(loan.endTime))); } return sb.toString(); } }); Map
  • 52. LiveData Transformations em@realm.io LiveData<List<LoanWithUserAndBook>> loanLiveData = mDb.loanModel().findLoansByNameAfter("Mike", getYesterdayDate()); // Instead of exposing the list of Loans, we can apply a transformation and expose Strings. LiveData<String> stringLiveData = Transformations.map(loanLiveData, new Function<List<LoanWithUserAndBook>, String>() { @Override public String apply(List<LoanWithUserAndBook> loansWithUserAndBook) { StringBuilder sb = new StringBuilder(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US); for (LoanWithUserAndBook loan : loansWithUserAndBook) { sb.append(String.format("%sn (Returned: %s)n", loan.bookTitle, simpleDateFormat.format(loan.endTime))); } return sb.toString(); } }); Map
  • 53. LiveData em@realm.io Subscribing to LiveData // Here an Activity, Subscribed to LiveData
 public class CustomResultUserActivity extends AppCompatActivity { private CustomResultViewModel mShowUserViewModel; private TextView mBooksTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.db_activity); mBooksTextView = (TextView) findViewById(R.id.books_tv); mShowUserViewModel = ViewModelProviders.of(this).get(CustomResultViewModel.class); mShowUserViewModel.getLoansResult().observe(this, new Observer<String>() { @Override public void onChanged(@Nullable final String result) { mBooksTextView.setText(result); } }); } }
  • 54. LiveData em@realm.io Subscribing to LiveData // Here an Activity, Subscribed to LiveData
 public class CustomResultUserActivity extends AppCompatActivity { private CustomResultViewModel mShowUserViewModel; private TextView mBooksTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.db_activity); mBooksTextView = (TextView) findViewById(R.id.books_tv); mShowUserViewModel = ViewModelProviders.of(this).get(CustomResultViewModel.class); mShowUserViewModel.getLoansResult().observe(this, new Observer<String>() { @Override public void onChanged(@Nullable final String result) { mBooksTextView.setText(result); } }); } } LiveData respects lifecycle of
  • 55. LiveData em@realm.io public class RealmLiveData<T extends RealmModel> extends LiveData<RealmResults<T>> { private RealmResults<T> mResults; private RealmChangeListener<RealmResults<T>> listener = new RealmChangeListener<RealmResults<T>>() { @Override public void onChange(RealmResults<T> results) { setValue(results); } }; public RealmLiveData(RealmResults<T> mResults) { this.mResults = mResults; } @Override protected void onActive() { mResults.addChangeListener(listener); } @Override protected void onInactive() { mResults.removeChangeListener(listener); } } Extensible - Represent anything as LiveData
  • 56. LiveData em@realm.io public class RealmLiveData<T extends RealmModel> extends LiveData<RealmResults<T>> { private RealmResults<T> mResults; private RealmChangeListener<RealmResults<T>> listener = new RealmChangeListener<RealmResults<T>>() { @Override public void onChange(RealmResults<T> results) { setValue(results); } }; public RealmLiveData(RealmResults<T> mResults) { this.mResults = mResults; } @Override protected void onActive() { mResults.addChangeListener(listener); } @Override protected void onInactive() { mResults.removeChangeListener(listener); } }
  • 57. LiveData em@realm.io public class RealmLiveData<T extends RealmModel> extends LiveData<RealmResults<T>> { private RealmResults<T> mResults; private RealmChangeListener<RealmResults<T>> listener = new RealmChangeListener<RealmResults<T>>() { @Override public void onChange(RealmResults<T> results) { setValue(results); } }; public RealmLiveData(RealmResults<T> mResults) { this.mResults = mResults; } @Override protected void onActive() { mResults.addChangeListener(listener); } @Override protected void onInactive() { mResults.removeChangeListener(listener); } }
  • 58. LiveData em@realm.io // Use Custom LiveData
 public RealmLiveData<Loan> findLoansByNameAfter(final String userName, final Date after) { return new RealmLiveData<>(mRealm.where(Loan.class) .like("user.name", userName) .greaterThan("endTime", after) .findAllAsync()); } // Can even transform that live data RealmLiveData<Loan> loans = findLoansByNameAfter(“Mike”, getYesterdayDate()); LiveData<String> mLoansResult = Transformations.map(loans, new Function<RealmResults<Loan>, String>() { @Override public String apply(RealmResults<Loan> loans) { StringBuilder sb = new StringBuilder(); for (Loan loan : loans) { sb.append(String.format("%sn (Returned: %s)n", loan.getBook().getTitle(), simpleDateFormat.format(loan.getEndTime()))); } return sb.toString(); } });
  • 59. LiveData em@realm.io // Use Custom LiveData
 public RealmLiveData<Loan> findLoansByNameAfter(final String userName, final Date after) { return new RealmLiveData<>(mRealm.where(Loan.class) .like("user.name", userName) .greaterThan("endTime", after) .findAllAsync()); } // Can even transform that live data RealmLiveData<Loan> loans = findLoansByNameAfter(“Mike”, getYesterdayDate()); LiveData<String> mLoansResult = Transformations.map(loans, new Function<RealmResults<Loan>, String>() { @Override public String apply(RealmResults<Loan> loans) { StringBuilder sb = new StringBuilder(); for (Loan loan : loans) { sb.append(String.format("%sn (Returned: %s)n", loan.getBook().getTitle(), simpleDateFormat.format(loan.getEndTime()))); } return sb.toString(); } });
  • 60. LiveData em@realm.io // Use Custom LiveData
 public RealmLiveData<Loan> findLoansByNameAfter(final String userName, final Date after) { return new RealmLiveData<>(mRealm.where(Loan.class) .like("user.name", userName) .greaterThan("endTime", after) .findAllAsync()); } // Can even transform that live data RealmLiveData<Loan> loans = findLoansByNameAfter(“Mike”, getYesterdayDate()); LiveData<String> mLoansResult = Transformations.map(loans, new Function<RealmResults<Loan>, String>() { @Override public String apply(RealmResults<Loan> loans) { StringBuilder sb = new StringBuilder(); for (Loan loan : loans) { sb.append(String.format("%sn (Returned: %s)n", loan.getBook().getTitle(), simpleDateFormat.format(loan.getEndTime()))); } return sb.toString(); } });
  • 62. em@realm.io Room • ORM for SQLite • DAOs defined in interface are generated at compile time • Results are Live(Data)
  • 64. em@realm.io Room public class LoanWithUserAndBook { public String id; @ColumnInfo(name="title") public String bookTitle; @ColumnInfo(name="name") public String userName; @TypeConverters(DateConverter.class) public Date startTime; @TypeConverters(DateConverter.class) public Date endTime; }
  • 65. em@realm.io Room @Entity(foreignKeys = { @ForeignKey(entity = Book.class, parentColumns = "id", childColumns = "book_id"), @ForeignKey(entity = User.class, parentColumns = "id", childColumns = "user_id")}) @TypeConverters(DateConverter.class) public class Loan { public @PrimaryKey String id; public Date startTime; public Date endTime; @ColumnInfo(name="book_id") public String bookId; @ColumnInfo(name="user_id") public String userId; } Define Entites @Entity public class User { public @PrimaryKey String id; public String name; public String lastName; public int age; } @Entity public class Book { public @PrimaryKey String id; public String title; }
  • 66. em@realm.io Room @Database(entities = {User.class, Book.class, Loan.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { private static AppDatabase INSTANCE; public abstract UserDao userModel(); public abstract BookDao bookModel(); public abstract LoanDao loanModel(); public static AppDatabase getInMemoryDatabase(Context context) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "LibraryDb") .build(); } return INSTANCE; } public static void destroyInstance() { INSTANCE = null; } } Define Database
  • 67. em@realm.io Room @Database(entities = {User.class, Book.class, Loan.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { private static AppDatabase INSTANCE; public abstract UserDao userModel(); public abstract BookDao bookModel(); public abstract LoanDao loanModel(); public static AppDatabase getInMemoryDatabase(Context context) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "LibraryDb") .build(); } return INSTANCE; } public static void destroyInstance() { INSTANCE = null; } } Define Database
  • 68. em@realm.io Room @Dao @TypeConverters(DateConverter.class) public interface LoanDao { @Query("SELECT * From Loan") LiveData<List<Loan>> findAll(); @Query("SELECT Loan.id, Book.title, User.name, Loan.startTime, Loan.endTime From Loan " + "INNER JOIN Book ON Loan.book_id = Book.id " + "INNER JOIN User ON Loan.user_id = User.id ") LiveData<List<LoanWithUserAndBook>> findAllWithUserAndBook(); @Query("SELECT Loan.id, Book.title as title, User.name as name, Loan.startTime, Loan.endTime " + "FROM Book " + "INNER JOIN Loan ON Loan.book_id = Book.id " + "INNER JOIN User on User.id = Loan.user_id " + "WHERE User.name LIKE :userName " + "AND Loan.endTime > :after " ) public LiveData<List<LoanWithUserAndBook>> findLoansByNameAfter(String userName, Date after); @Insert(onConflict = ABORT) void insertLoan(Loan loan); @Query("DELETE FROM Loan") void deleteAll(); } Define DAO Interface
  • 69. em@realm.io Room public class CustomResultViewModel extends AndroidViewModel { private AppDatabase mDb; public CustomResultViewModel(Application application) { super(application); mDb = AppDatabase.getDatabase(application); } public logMikesLoans() { LiveData<List<LoanWithUserAndBook>> loans = mDb.loanModel().findLoansByNameAfter("Mike", getYesterdayDate()); ... } Using Room DAOs
  • 70. em@realm.io Room @RunWith(AndroidJUnit4.class) public class SimpleEntityReadWriteTest { private UserDao mUserDao; private TestDatabase mDb; @Before public void createDb() { Context context = InstrumentationRegistry.getTargetContext(); mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build(); mUserDao = mDb.getUserDao(); } @After public void closeDb() throws IOException { mDb.close(); } @Test public void writeUserAndReadInList() throws Exception { User user = TestUtil.createUser(3); user.setName("george"); mUserDao.insert(user); List<User> byName = mUserDao.findUsersByName("george"); assertThat(byName.get(0), equalTo(user)); } } Room Test Support https://developer.android.com/topic/libraries/architecture/room.html#testing-android
  • 72. Architecture “It is impossible to have one way of writing apps that will be the best for every scenario. If you already have a good way of writing Android apps, you don't need to change. That being said, this recommended architecture should be a good starting point for most use cases.” https://developer.android.com/topic/libraries/architecture/guide.html#recommended_app_architecture
  • 73. Official Android Architecture Dogma Guidance For more information see: https://developer.android.com/topic/libraries/architecture/guide.html
  • 74. General Good Practices • Avoid storing state or data in Activities, Fragments, Services, Broadcast Receivers • Keep a strong separation of concerns • Build your Data, Model, ViewModel, Activity/Fragment, etc. as components with strong inputs / outputs • Android Architecture Components and libraries. Focus on what makes your app unique • Build your apps UI/UX to function well when offline • Use Service Locators or Dependency Injection for dependencies
  • 75. Additional Materials • Android Architecture Components • https://developer.android.com/topic/libraries/architecture/index.html • https://github.com/googlesamples/android-architecture-components • https://github.com/googlesamples/android-architecture (Architecture Blueprints) • Other Reactive Uni-Directional Architectures • Model View Intent - http://hannesdorfmann.com/android/mosby3-mvi-2#model-view-intent-mvi • FLUX on Android - https://github.com/lgvalle/android-flux-todo-app • Other Articles I’ve published on these topics • https://news.realm.io/news/eric-maxwell-mvc-mvp-and-mvvm-on-android/ • https://news.realm.io/news/eric-maxwell-uni-directional-architecture-android-using-realm • https://news.realm.io/news/android-architecture-components-and-realm/ • Samples • https://github.com/ericmaxwell2003/android-architecture-samples