├── .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>() { 61 | @Override 62 | public ObservableSource apply(@NonNull Object o) throws Exception { 63 | return new ObservableSource() { 64 | @Override 65 | public void subscribe(@NonNull Observer observer) { 66 | observer.onNext(new DefaultViewClick(view.getId())); 67 | } 68 | }; 69 | } 70 | }).subscribe(controller.click()); 71 | } 72 | 73 | protected abstract C createController(V viewModel); 74 | 75 | protected abstract Class viewModel(); 76 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/ControllerComponent.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller; 2 | 3 | public interface ControllerComponent { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/DefaultApplication.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller; 2 | 3 | import android.app.Application; 4 | 5 | import com.memtrip.mvc.controller.app.cake.CakeComponent; 6 | import com.memtrip.mvc.controller.app.cake.DaggerCakeComponent; 7 | import com.memtrip.mvc.repository.NetworkModule; 8 | import com.memtrip.mvc.repository.cake.CakeRepositoryModule; 9 | import com.memtrip.mvc.repository.cake.api.CakeApiModule; 10 | import com.memtrip.mvc.system.RxModule; 11 | 12 | import static com.memtrip.mvc.controller.app.cake.CakeComponent.CAKE_COMPONENT; 13 | 14 | public class DefaultApplication extends Application { 15 | 16 | private CakeComponent cakeComponent; 17 | 18 | @Override 19 | public void onCreate() { 20 | super.onCreate(); 21 | 22 | cakeComponent = DaggerCakeComponent 23 | .builder() 24 | .rxModule(new RxModule()) 25 | .networkModule(new NetworkModule(this)) 26 | .cakeApiModule(new CakeApiModule()) 27 | .cakeRepositoryModule(new CakeRepositoryModule()) 28 | .build(); 29 | } 30 | 31 | @Override 32 | public Object getSystemService(String name) { 33 | if (CAKE_COMPONENT.equals(name)) { 34 | return cakeComponent; 35 | } else { 36 | return super.getSystemService(name); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/UiViewModel.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller; 2 | 3 | import android.arch.lifecycle.MutableLiveData; 4 | import android.arch.lifecycle.ViewModel; 5 | 6 | public abstract class UiViewModel extends ViewModel { 7 | 8 | private MutableLiveData showProgress; 9 | 10 | protected UiViewModel() { 11 | showProgress = new MutableLiveData<>(); 12 | } 13 | 14 | public MutableLiveData showProgress() { 15 | return showProgress; 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/app/cake/CakeActivity.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller.app.cake; 2 | 3 | import android.arch.lifecycle.Observer; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.View; 9 | import android.widget.ProgressBar; 10 | 11 | import com.memtrip.mvc.R; 12 | import com.memtrip.mvc.controller.ControllerActivity; 13 | import com.memtrip.mvc.controller.interaction.model.ErrorModel; 14 | import com.memtrip.mvc.controller.ui.FrameErrorView; 15 | import com.memtrip.mvc.system.entity.Cake; 16 | 17 | import java.util.List; 18 | 19 | import butterknife.BindView; 20 | import butterknife.ButterKnife; 21 | 22 | import static com.memtrip.mvc.controller.app.cake.CakeComponent.CAKE_COMPONENT; 23 | 24 | public class CakeActivity extends ControllerActivity { 25 | 26 | @BindView(R.id.cake_activity_frame_error) 27 | FrameErrorView errorFrame; 28 | 29 | @BindView(R.id.cake_activity_recycler_view) 30 | RecyclerView recyclerView; 31 | 32 | @BindView(R.id.cake_activity_progress_bar) 33 | ProgressBar progressBar; 34 | 35 | private CakesAdapter cakesAdapter; 36 | 37 | @Override 38 | protected void onCreate(@Nullable Bundle savedInstanceState) { 39 | super.onCreate(savedInstanceState); 40 | setContentView(R.layout.cake_activity); 41 | ButterKnife.bind(this); 42 | 43 | cakesAdapter = new CakesAdapter(this); 44 | 45 | recyclerView.setAdapter(cakesAdapter); 46 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 47 | 48 | observeClicks(errorFrame.click()); 49 | } 50 | 51 | @Override 52 | protected void observe(CakeViewModel viewModel) { 53 | super.observe(viewModel); 54 | 55 | viewModel.error().observe(this, new Observer() { 56 | @Override 57 | public void onChanged(@Nullable ErrorModel errorModel) { 58 | errorFrame.setVisibility(View.VISIBLE); 59 | errorFrame.title().setText(getResources().getString(errorModel.title().id())); 60 | errorFrame.body().setText(getResources().getString(errorModel.body().id())); 61 | } 62 | }); 63 | 64 | viewModel.showProgress().observe(this, new Observer() { 65 | @Override 66 | public void onChanged(@Nullable Boolean showProgress) { 67 | errorFrame.setVisibility(View.GONE); 68 | progressBar.setVisibility(showProgress ? View.VISIBLE : View.GONE); 69 | } 70 | }); 71 | 72 | viewModel.cakes().observe(this, new Observer>() { 73 | @Override 74 | public void onChanged(@Nullable List cakes) { 75 | cakesAdapter.setData(cakes); 76 | recyclerView.setVisibility(View.VISIBLE);; 77 | } 78 | }); 79 | } 80 | 81 | @Override 82 | protected CakeController createController(CakeViewModel viewModel) { 83 | 84 | CakeController cakePresenter = new CakeController(viewModel); 85 | 86 | injector(CAKE_COMPONENT).inject(cakePresenter); 87 | 88 | return cakePresenter; 89 | } 90 | 91 | @Override 92 | protected Class viewModel() { 93 | return CakeViewModel.class; 94 | } 95 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/app/cake/CakeComponent.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller.app.cake; 2 | 3 | import com.memtrip.mvc.controller.ControllerComponent; 4 | import com.memtrip.mvc.repository.NetworkModule; 5 | import com.memtrip.mvc.repository.cake.CakeRepositoryModule; 6 | import com.memtrip.mvc.repository.cake.api.CakeApiModule; 7 | import com.memtrip.mvc.system.RxModule; 8 | 9 | import dagger.Component; 10 | 11 | @Component(modules = { 12 | RxModule.class, 13 | NetworkModule.class, 14 | CakeApiModule.class, 15 | CakeRepositoryModule.class 16 | 17 | }) 18 | public interface CakeComponent extends ControllerComponent { 19 | 20 | String CAKE_COMPONENT = "CAKE_COMPONENT"; 21 | 22 | void inject(CakeController controller); 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/app/cake/CakeController.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller.app.cake; 2 | 3 | import com.memtrip.mvc.R; 4 | import com.memtrip.mvc.controller.Controller; 5 | import com.memtrip.mvc.controller.interaction.ViewClick; 6 | import com.memtrip.mvc.controller.interaction.model.ErrorModel; 7 | import com.memtrip.mvc.controller.interaction.model.res.StringResData; 8 | import com.memtrip.mvc.repository.cake.CakeRepository; 9 | import com.memtrip.mvc.system.entity.Cake; 10 | 11 | import java.util.List; 12 | 13 | import javax.inject.Inject; 14 | import javax.inject.Named; 15 | 16 | import io.reactivex.Scheduler; 17 | import io.reactivex.SingleObserver; 18 | import io.reactivex.annotations.NonNull; 19 | import io.reactivex.disposables.Disposable; 20 | import io.reactivex.functions.Consumer; 21 | 22 | class CakeController extends Controller { 23 | 24 | @Inject 25 | CakeRepository cakeRepository; 26 | 27 | @Inject 28 | @Named("mainThread") 29 | Scheduler mainThread; 30 | 31 | @Inject 32 | @Named("background") 33 | Scheduler background; 34 | 35 | private Disposable disposable; 36 | 37 | CakeController(CakeViewModel viewModel) { 38 | super(viewModel); 39 | } 40 | 41 | @Override 42 | protected void onStart() { 43 | initialCakes(); 44 | } 45 | 46 | private void initialCakes() { 47 | 48 | if (!viewModel().cakes().exists()) { 49 | 50 | viewModel().showProgress().setValue(true); 51 | 52 | cakeRepository.cakes() 53 | .observeOn(mainThread) 54 | .subscribeOn(background) 55 | .subscribe(new SingleObserver>() { 56 | @Override 57 | public void onSubscribe(@NonNull Disposable d) { 58 | disposable = d; 59 | } 60 | 61 | @Override 62 | public void onSuccess(@NonNull List cakes) { 63 | success(cakes); 64 | } 65 | 66 | @Override 67 | public void onError(@NonNull Throwable e) { 68 | failure(); 69 | } 70 | }); 71 | } 72 | } 73 | 74 | private void success(List cakes) { 75 | 76 | viewModel().showProgress().setValue(false); 77 | 78 | viewModel().cakes().setValue(cakes); 79 | } 80 | 81 | private void failure() { 82 | 83 | viewModel().showProgress().setValue(false); 84 | 85 | viewModel().error().setValue(new ErrorModel( 86 | new StringResData(R.string.app_error_title_generic), 87 | new StringResData(R.string.app_error_body_generic) 88 | )); 89 | } 90 | 91 | @Override 92 | protected void onStop() { 93 | if (disposable != null) { 94 | disposable.dispose(); 95 | } 96 | } 97 | 98 | @Override 99 | public Consumer click() { 100 | return new Consumer() { 101 | @Override 102 | public void accept(@NonNull ViewClick viewClick) { 103 | switch (viewClick.id()) { 104 | case R.id.inline_error_retry: 105 | initialCakes(); 106 | break; 107 | } 108 | } 109 | }; 110 | } 111 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/app/cake/CakeViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller.app.cake; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.View; 5 | import android.widget.TextView; 6 | 7 | import com.memtrip.mvc.R; 8 | import com.memtrip.mvc.system.entity.Cake; 9 | 10 | import butterknife.BindView; 11 | import butterknife.ButterKnife; 12 | 13 | class CakeViewHolder extends RecyclerView.ViewHolder { 14 | 15 | @BindView(R.id.cake_adapter_title) 16 | TextView titleTextView; 17 | 18 | @BindView(R.id.cake_adapter_body) 19 | TextView bodyTextView; 20 | 21 | CakeViewHolder(View itemView) { 22 | super(itemView); 23 | ButterKnife.bind(this, itemView); 24 | } 25 | 26 | void populate(Cake cake) { 27 | titleTextView.setText(cake.title()); 28 | bodyTextView.setText(cake.desc()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/app/cake/CakeViewModel.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller.app.cake; 2 | 3 | import android.arch.lifecycle.MutableLiveData; 4 | 5 | import com.memtrip.mvc.controller.UiViewModel; 6 | import com.memtrip.mvc.controller.interaction.model.ErrorModel; 7 | import com.memtrip.mvc.controller.interaction.model.ExistsLiveData; 8 | import com.memtrip.mvc.system.entity.Cake; 9 | 10 | import java.util.List; 11 | 12 | class CakeViewModel extends UiViewModel { 13 | 14 | private ExistsLiveData> cakes; 15 | private MutableLiveData error; 16 | 17 | public CakeViewModel() { 18 | cakes = new ExistsLiveData<>(); 19 | error = new MutableLiveData<>(); 20 | } 21 | 22 | ExistsLiveData> cakes() { 23 | return cakes; 24 | } 25 | 26 | MutableLiveData error() { 27 | return error; 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/app/cake/CakesAdapter.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller.app.cake; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.ViewGroup; 6 | 7 | import com.memtrip.mvc.R; 8 | import com.memtrip.mvc.controller.ui.ListAdapter; 9 | import com.memtrip.mvc.system.entity.Cake; 10 | 11 | class CakesAdapter extends ListAdapter { 12 | 13 | private final LayoutInflater inflater; 14 | 15 | CakesAdapter(Context context) { 16 | inflater = LayoutInflater.from(context); 17 | } 18 | 19 | @Override 20 | public CakeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 21 | return new CakeViewHolder(inflater.inflate(R.layout.cake_adapter, null)); 22 | } 23 | 24 | @Override 25 | public void onBindViewHolder(CakeViewHolder holder, int position) { 26 | holder.populate(data().get(position)); 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/interaction/DefaultViewClick.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller.interaction; 2 | 3 | public class DefaultViewClick implements ViewClick { 4 | 5 | private final int id; 6 | 7 | public DefaultViewClick(int id) { 8 | this.id = id; 9 | } 10 | 11 | @Override 12 | public int id() { 13 | return id; 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/interaction/ViewClick.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller.interaction; 2 | 3 | public interface ViewClick { 4 | 5 | int id(); 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/interaction/model/ErrorModel.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller.interaction.model; 2 | 3 | import com.memtrip.mvc.controller.interaction.model.res.StringResData; 4 | 5 | public class ErrorModel { 6 | 7 | private final StringResData title; 8 | private final StringResData body; 9 | 10 | public StringResData title() { 11 | return title; 12 | } 13 | 14 | public StringResData body() { 15 | return body; 16 | } 17 | 18 | public ErrorModel(StringResData title, StringResData body) { 19 | this.title = title; 20 | this.body = body; 21 | } 22 | 23 | @Override 24 | public boolean equals(Object o) { 25 | if (o instanceof ErrorModel) { 26 | ErrorModel that = (ErrorModel) o; 27 | 28 | return title.equals(that.title) 29 | && body.equals(that.body); 30 | } else { 31 | return false; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/interaction/model/ExistsLiveData.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller.interaction.model; 2 | 3 | import android.arch.lifecycle.MutableLiveData; 4 | 5 | public class ExistsLiveData extends MutableLiveData { 6 | 7 | private boolean exists; 8 | 9 | public boolean exists() { 10 | return exists; 11 | } 12 | 13 | @Override 14 | public void setValue(T value) { 15 | super.setValue(value); 16 | 17 | exists = true; 18 | } 19 | 20 | @Override 21 | public void postValue(T value) { 22 | super.postValue(value); 23 | 24 | exists = true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/interaction/model/res/StringResData.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller.interaction.model.res; 2 | 3 | import android.support.annotation.StringRes; 4 | 5 | public class StringResData { 6 | 7 | private final @StringRes int id; 8 | 9 | public @StringRes int id() { 10 | return id; 11 | } 12 | 13 | public StringResData(@StringRes int id) { 14 | this.id = id; 15 | } 16 | 17 | @Override 18 | public boolean equals(Object o) { 19 | 20 | if (o instanceof StringResData) { 21 | StringResData that = (StringResData) o; 22 | return id == that.id; 23 | } else { 24 | return false; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/ui/FrameErrorView.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller.ui; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.AttrRes; 5 | import android.support.annotation.NonNull; 6 | import android.support.annotation.Nullable; 7 | import android.util.AttributeSet; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.widget.Button; 11 | import android.widget.FrameLayout; 12 | import android.widget.TextView; 13 | 14 | import com.memtrip.mvc.R; 15 | 16 | import butterknife.BindView; 17 | import butterknife.ButterKnife; 18 | 19 | public class FrameErrorView extends FrameLayout { 20 | 21 | @BindView(R.id.inline_error_title) 22 | TextView titleTextView; 23 | 24 | @BindView(R.id.inline_error_body) 25 | TextView bodyTextView; 26 | 27 | @BindView(R.id.inline_error_retry) 28 | Button retryButton; 29 | 30 | public TextView title() { 31 | return titleTextView; 32 | } 33 | 34 | public TextView body() { 35 | return bodyTextView; 36 | } 37 | 38 | public FrameErrorView(@NonNull Context context) { 39 | this(context, null); 40 | } 41 | 42 | public FrameErrorView(@NonNull Context context, @Nullable AttributeSet attrs) { 43 | this(context, attrs, 0); 44 | } 45 | 46 | public FrameErrorView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { 47 | super(context, attrs, defStyleAttr); 48 | LayoutInflater inflater = LayoutInflater.from(context); 49 | inflater.inflate(R.layout.app_inline_error, this); 50 | ButterKnife.bind(this); 51 | } 52 | 53 | public View click() { 54 | return retryButton; 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/controller/ui/ListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.controller.ui; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public abstract class ListAdapter 9 | extends RecyclerView.Adapter { 10 | 11 | private List data = new ArrayList<>(); 12 | 13 | protected List data() { 14 | return data; 15 | } 16 | 17 | public void setData(List data) { 18 | this.data = data; 19 | } 20 | 21 | @Override 22 | public int getItemCount() { 23 | return data.size(); 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/repository/NetworkModule.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.repository; 2 | 3 | import android.content.Context; 4 | 5 | import com.fasterxml.jackson.databind.DeserializationFeature; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.SerializationFeature; 8 | import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 9 | import com.memtrip.mvc.R; 10 | 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import javax.inject.Named; 14 | 15 | import dagger.Module; 16 | import dagger.Provides; 17 | import okhttp3.OkHttpClient; 18 | import okhttp3.logging.HttpLoggingInterceptor; 19 | import retrofit2.Retrofit; 20 | import retrofit2.converter.jackson.JacksonConverterFactory; 21 | 22 | @Module 23 | public class NetworkModule { 24 | 25 | private final Context context; 26 | 27 | public NetworkModule(Context context) { 28 | this.context = context; 29 | } 30 | 31 | @Provides 32 | OkHttpClient okHttpClient() { 33 | 34 | HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); 35 | interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); 36 | 37 | int timeout = context.getResources().getInteger(R.integer.app_server_timeout); 38 | 39 | return new OkHttpClient.Builder() 40 | .addInterceptor(interceptor) 41 | .connectTimeout(timeout, TimeUnit.SECONDS) 42 | .build(); 43 | } 44 | 45 | @Provides 46 | ObjectMapper objectMapper() { 47 | ObjectMapper mapper = new ObjectMapper(); 48 | 49 | mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); 50 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 51 | 52 | return mapper; 53 | } 54 | 55 | @Provides 56 | retrofit2.Converter.Factory converterFactory(ObjectMapper mapper) { 57 | return JacksonConverterFactory.create(mapper); 58 | } 59 | 60 | @Provides 61 | Retrofit retrofit(@Named("apiEndpoint") String endpoint, 62 | OkHttpClient client, 63 | retrofit2.Converter.Factory converterFactory) { 64 | 65 | return new Retrofit.Builder() 66 | .baseUrl(endpoint) 67 | .client(client) 68 | .addConverterFactory(converterFactory) 69 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 70 | .build(); 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/repository/cake/CakeRepository.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.repository.cake; 2 | 3 | import com.memtrip.mvc.system.entity.Cake; 4 | 5 | import java.util.List; 6 | 7 | import io.reactivex.Single; 8 | 9 | public interface CakeRepository { 10 | 11 | Single> cakes(); 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/repository/cake/CakeRepositoryModule.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.repository.cake; 2 | 3 | import com.memtrip.mvc.repository.cake.api.CakeApi; 4 | import com.memtrip.mvc.repository.cake.api.CakeModel; 5 | import com.memtrip.mvc.system.entity.Cake; 6 | import com.memtrip.mvc.system.entity.convert.ConvertTo; 7 | 8 | import java.util.List; 9 | 10 | import dagger.Module; 11 | import dagger.Provides; 12 | 13 | @Module 14 | public class CakeRepositoryModule { 15 | 16 | @Provides 17 | CakeRepository cakeRepository(CakeApi cakeApi, ConvertTo, List> convertTo) { 18 | return new DefaultCakeRepository(cakeApi, convertTo); 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/repository/cake/DefaultCakeRepository.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.repository.cake; 2 | 3 | import com.memtrip.mvc.repository.cake.api.CakeApi; 4 | import com.memtrip.mvc.repository.cake.api.CakeModel; 5 | import com.memtrip.mvc.system.entity.Cake; 6 | import com.memtrip.mvc.system.entity.convert.ConvertTo; 7 | 8 | import java.util.List; 9 | 10 | import io.reactivex.Single; 11 | import io.reactivex.SingleObserver; 12 | import io.reactivex.SingleSource; 13 | import io.reactivex.annotations.NonNull; 14 | import io.reactivex.functions.Function; 15 | 16 | class DefaultCakeRepository implements CakeRepository { 17 | 18 | private final CakeApi cakeApi; 19 | private final ConvertTo, List> convertToCake; 20 | 21 | DefaultCakeRepository(CakeApi cakeApi, ConvertTo, List> convertToCake) { 22 | this.cakeApi = cakeApi; 23 | this.convertToCake = convertToCake; 24 | } 25 | 26 | @Override 27 | public Single> cakes() { 28 | return cakesFromApi(); 29 | } 30 | 31 | private Single> cakesFromApi() { 32 | return cakeApi.cakes().flatMap(new Function, SingleSource>>() { 33 | @Override 34 | public SingleSource> apply(final @NonNull List cakeModels) { 35 | return new Single>() { 36 | @Override 37 | protected void subscribeActual(@NonNull SingleObserver> observer) { 38 | observer.onSuccess(convertToCake.from(cakeModels)); 39 | } 40 | }; 41 | } 42 | }); 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/repository/cake/api/CakeApi.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.repository.cake.api; 2 | 3 | import java.util.List; 4 | 5 | import io.reactivex.Single; 6 | import retrofit2.http.GET; 7 | 8 | public interface CakeApi { 9 | 10 | @GET("6c48e1595fb57bed78f2152790b1521e/raw/594dd98f5d93afb0e8e62c2be766536250623b1f/cakes.json") 11 | Single> cakes(); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/repository/cake/api/CakeApiModule.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.repository.cake.api; 2 | 3 | import com.memtrip.mvc.system.entity.Cake; 4 | import com.memtrip.mvc.system.entity.convert.ConvertTo; 5 | 6 | import java.util.List; 7 | 8 | import javax.inject.Named; 9 | 10 | import dagger.Module; 11 | import dagger.Provides; 12 | import retrofit2.Retrofit; 13 | 14 | @Module 15 | public class CakeApiModule { 16 | 17 | @Provides 18 | @Named("apiEndpoint") 19 | String endPoint() { 20 | return "https://gist.githubusercontent.com/samkirton/"; 21 | } 22 | 23 | @Provides 24 | CakeApi weatherApi(Retrofit retrofit) { 25 | return retrofit.create(CakeApi.class); 26 | } 27 | 28 | @Provides 29 | ConvertTo, List> convertToCake() { 30 | return new ConvertToCake(); 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/repository/cake/api/CakeModel.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.repository.cake.api; 2 | 3 | public class CakeModel { 4 | private String title; 5 | private String desc; 6 | 7 | public String getTitle() { 8 | return title; 9 | } 10 | 11 | public void setTitle(String title) { 12 | this.title = title; 13 | } 14 | 15 | public String getDesc() { 16 | return desc; 17 | } 18 | 19 | public void setDesc(String desc) { 20 | this.desc = desc; 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/repository/cake/api/ConvertToCake.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.repository.cake.api; 2 | 3 | import com.memtrip.mvc.system.entity.Cake; 4 | import com.memtrip.mvc.system.entity.convert.ConvertTo; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class ConvertToCake implements ConvertTo, List> { 10 | 11 | @Override 12 | public List from(List models) { 13 | 14 | List cakes = new ArrayList<>(); 15 | 16 | for (CakeModel model : models) { 17 | cakes.add(new Cake( 18 | model.getTitle(), 19 | model.getDesc()) 20 | ); 21 | } 22 | 23 | return cakes; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/system/RxModule.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.system; 2 | 3 | import javax.inject.Named; 4 | 5 | import dagger.Module; 6 | import dagger.Provides; 7 | import io.reactivex.Scheduler; 8 | import io.reactivex.android.schedulers.AndroidSchedulers; 9 | import io.reactivex.schedulers.Schedulers; 10 | 11 | @Module 12 | public class RxModule { 13 | 14 | @Provides 15 | @Named("mainThread") 16 | public Scheduler mainThreadScheduler() { 17 | return AndroidSchedulers.mainThread(); 18 | } 19 | 20 | @Provides 21 | @Named("background") 22 | public Scheduler backgroundScheduler() { 23 | return Schedulers.io(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/system/entity/Cake.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.system.entity; 2 | 3 | public class Cake { 4 | 5 | private final String title; 6 | private final String description; 7 | 8 | public String title() { 9 | return title; 10 | } 11 | 12 | public String desc() { 13 | return description; 14 | } 15 | 16 | public Cake(String title, String description) { 17 | this.title = title; 18 | this.description = description; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/memtrip/mvc/system/entity/convert/ConvertTo.java: -------------------------------------------------------------------------------- 1 | package com.memtrip.mvc.system.entity.convert; 2 | 3 | public interface ConvertTo { 4 | 5 | E from(M model); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/app_inline_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 30 | 31 |