├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── memtrip
│ │ │ └── mvc
│ │ │ ├── MainActivity.java
│ │ │ ├── controller
│ │ │ ├── Controller.java
│ │ │ ├── ControllerActivity.java
│ │ │ ├── ControllerComponent.java
│ │ │ ├── DefaultApplication.java
│ │ │ ├── UiViewModel.java
│ │ │ ├── app
│ │ │ │ └── cake
│ │ │ │ │ ├── CakeActivity.java
│ │ │ │ │ ├── CakeComponent.java
│ │ │ │ │ ├── CakeController.java
│ │ │ │ │ ├── CakeViewHolder.java
│ │ │ │ │ ├── CakeViewModel.java
│ │ │ │ │ └── CakesAdapter.java
│ │ │ ├── interaction
│ │ │ │ ├── DefaultViewClick.java
│ │ │ │ ├── ViewClick.java
│ │ │ │ └── model
│ │ │ │ │ ├── ErrorModel.java
│ │ │ │ │ ├── ExistsLiveData.java
│ │ │ │ │ └── res
│ │ │ │ │ └── StringResData.java
│ │ │ └── ui
│ │ │ │ ├── FrameErrorView.java
│ │ │ │ └── ListAdapter.java
│ │ │ ├── repository
│ │ │ ├── NetworkModule.java
│ │ │ └── cake
│ │ │ │ ├── CakeRepository.java
│ │ │ │ ├── CakeRepositoryModule.java
│ │ │ │ ├── DefaultCakeRepository.java
│ │ │ │ └── api
│ │ │ │ ├── CakeApi.java
│ │ │ │ ├── CakeApiModule.java
│ │ │ │ ├── CakeModel.java
│ │ │ │ └── ConvertToCake.java
│ │ │ └── system
│ │ │ ├── RxModule.java
│ │ │ └── entity
│ │ │ ├── Cake.java
│ │ │ └── convert
│ │ │ └── ConvertTo.java
│ └── res
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── app_inline_error.xml
│ │ ├── cake_activity.xml
│ │ └── cake_adapter.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── app_colors.xml
│ │ ├── app_dimens.xml
│ │ ├── app_integers.xml
│ │ ├── app_strings.xml
│ │ └── app_styles.xml
│ └── test
│ └── java
│ └── com
│ └── memtrip
│ └── mvc
│ ├── controller
│ ├── ControllerTests.java
│ └── app
│ │ └── cake
│ │ ├── CakeControllerTests.java
│ │ └── MockCakeComponent.java
│ ├── repository
│ ├── MockNetworkModule.java
│ └── cake
│ │ └── MockCakeRepositoryModule.java
│ └── system
│ └── MockRxModule.java
├── build.gradle
├── gradle.properties
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 | .idea
11 | gradle
12 | gradlew
13 | gradlew.bat
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## MVC
2 | Reactive Programming paradigms and `android.arch.lifecycle.ViewModel` allow for the following changes to MVC on Android:
3 |
4 | * UI elements listen to a ViewModel and ONLY update themselves when something changes.
5 | * Controllers listen for UI interactions and update the ViewModel accordingly.
6 | * Controllers hold a reference to a ViewModel rather than a View
7 | * Controllers only ever respond to ui interactions, a UI can not ask the controller to do something.
8 |
9 | ```
10 | Emit change
11 | ----------------------------------------------------
12 | | |
13 | | |
14 | | |
15 | --------v-------- ---------------- ----------------
16 | | | | | | |
17 | | | | | | |
18 | | | | | | |
19 | | | | | | |
20 | | (Model) | Listen | (View) | | (Controller) |
21 | | ViewModel *--------| Activity | | |
22 | | | | | | |
23 | | | | | | |
24 | | | | | | |
25 | | | | | | |
26 | ----------------- -------*-------- ----------------
27 | | |
28 | | |
29 | | |
30 | --------------------------
31 | Listen for UI interactions
32 |
33 | ```
34 |
35 | ### Unit testing
36 | The behaviour of the ViewModel can be easily tested, please see `com.memtrip.mvmp.presenter.app.cake.CakeControllerTests.java`
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 25
5 | buildToolsVersion "25.0.3"
6 | defaultConfig {
7 | applicationId "com.archetecture.latest"
8 | minSdkVersion 15
9 | targetSdkVersion 25
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
13 | }
14 |
15 | buildTypes {
16 | release {
17 | minifyEnabled false
18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
19 | }
20 | }
21 |
22 | packagingOptions {
23 | exclude 'META-INF/NOTICE'
24 | exclude 'META-INF/NOTICE.txt'
25 | exclude 'META-INF/LICENSE'
26 | exclude 'META-INF/LICENSE.txt'
27 | }
28 |
29 | testOptions {
30 | unitTests.returnDefaultValues = true
31 | }
32 |
33 |
34 | android.applicationVariants.all {
35 | def aptOutputDir = new File(buildDir, "generated/source/apt/${it.unitTestVariant.dirName}")
36 | it.unitTestVariant.addJavaSourceFoldersToModel(aptOutputDir)
37 | }
38 | }
39 |
40 | dependencies {
41 | compile fileTree(dir: 'libs', include: ['*.jar'])
42 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
43 | exclude group: 'com.android.support', module: 'support-annotations'
44 | })
45 | compile 'com.android.support:appcompat-v7:25.3.1'
46 | compile 'com.android.support:recyclerview-v7:25.3.1'
47 | compile 'com.android.support.constraint:constraint-layout:1.0.2'
48 |
49 | /**/
50 | compile 'com.squareup.retrofit2:retrofit:2.3.0'
51 | compile 'com.squareup.retrofit2:converter-jackson:2.3.0'
52 | compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
53 |
54 | /**/
55 | compile 'com.squareup.okhttp3:okhttp:3.4.1'
56 | compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
57 |
58 | /**/
59 | compile 'com.google.dagger:dagger:2.7'
60 | annotationProcessor 'com.google.dagger:dagger-compiler:2.7'
61 | testAnnotationProcessor 'com.google.dagger:dagger-compiler:2.7'
62 |
63 | /**/
64 | compile 'com.jakewharton:butterknife:8.6.0'
65 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0'
66 |
67 | /**/
68 | compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
69 |
70 | /**/
71 | compile 'io.reactivex.rxjava2:rxjava:2.1.0'
72 |
73 | /**/
74 | compile "android.arch.lifecycle:runtime:1.0.0-alpha1"
75 | compile "android.arch.lifecycle:extensions:1.0.0-alpha1"
76 | annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha1"
77 |
78 | /**/
79 | testCompile 'org.mockito:mockito-core:2.2.11'
80 | testCompile 'junit:junit:4.12'
81 | }
82 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/samkirton/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/memtrip/mvc/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.memtrip.mvc;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.app.AppCompatActivity;
5 |
6 | public class MainActivity extends AppCompatActivity {
7 |
8 | @Override
9 | protected void onCreate(Bundle savedInstanceState) {
10 | super.onCreate(savedInstanceState);
11 | setContentView(R.layout.activity_main);
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/memtrip/mvc/controller/Controller.java:
--------------------------------------------------------------------------------
1 | package com.memtrip.mvc.controller;
2 |
3 | import android.arch.lifecycle.Lifecycle;
4 | import android.arch.lifecycle.LifecycleObserver;
5 | import android.arch.lifecycle.OnLifecycleEvent;
6 |
7 | import com.memtrip.mvc.controller.interaction.ViewClick;
8 |
9 | import io.reactivex.functions.Consumer;
10 |
11 | public abstract class Controller implements LifecycleObserver {
12 |
13 | private final M viewModel;
14 |
15 | protected M viewModel() {
16 | return viewModel;
17 | }
18 |
19 | public Controller(M viewModel) {
20 | this.viewModel = viewModel;
21 | }
22 |
23 | protected Consumer click() {
24 | throw new IllegalStateException("click() must be overridden by the controller to handle clicks");
25 | }
26 |
27 | @OnLifecycleEvent(Lifecycle.Event.ON_START)
28 | protected void onStart() {
29 |
30 | }
31 |
32 | @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
33 | protected void onStop() {
34 |
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/memtrip/mvc/controller/ControllerActivity.java:
--------------------------------------------------------------------------------
1 | package com.memtrip.mvc.controller;
2 |
3 | import android.arch.lifecycle.LifecycleActivity;
4 | import android.arch.lifecycle.ViewModel;
5 | import android.arch.lifecycle.ViewModelProviders;
6 | import android.os.Bundle;
7 | import android.support.annotation.Nullable;
8 | import android.view.View;
9 |
10 | import com.jakewharton.rxbinding2.view.RxView;
11 | import com.memtrip.mvc.controller.interaction.DefaultViewClick;
12 | import com.memtrip.mvc.controller.interaction.ViewClick;
13 |
14 | import io.reactivex.ObservableSource;
15 | import io.reactivex.Observer;
16 | import io.reactivex.annotations.NonNull;
17 | import io.reactivex.functions.Function;
18 |
19 | public abstract class ControllerActivity
20 | extends LifecycleActivity {
21 |
22 | private V viewModel;
23 |
24 | private C controller;
25 |
26 | @SuppressWarnings("unchecked")
27 | protected DI injector(String name) {
28 | return (DI) getApplication().getSystemService(name);
29 | }
30 |
31 | @Override
32 | protected void onCreate(@Nullable Bundle savedInstanceState) {
33 | super.onCreate(savedInstanceState);
34 |
35 | viewModel = ViewModelProviders.of(this).get(viewModel());
36 |
37 | controller = createController(viewModel);
38 |
39 | getLifecycle().addObserver(controller);
40 | }
41 |
42 | @Override
43 | protected void onStart() {
44 | super.onStart();
45 |
46 | observe(viewModel);
47 | }
48 |
49 | protected void observe(V viewModel) {
50 |
51 | }
52 |
53 | protected void observeClicks(View ... views) {
54 | for (View view : views) {
55 | observeClicks(view);
56 | }
57 | }
58 |
59 | protected void observeClicks(final View view) {
60 | RxView.clicks(view).flatMap(new Function