├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── techyourchance │ │ └── mvc │ │ ├── common │ │ ├── BaseObservable.java │ │ ├── Constants.java │ │ ├── CustomApplication.java │ │ ├── dependencyinjection │ │ │ ├── ActivityCompositionRoot.java │ │ │ ├── CompositionRoot.java │ │ │ └── ControllerCompositionRoot.java │ │ └── permissions │ │ │ └── PermissionsHelper.java │ │ ├── networking │ │ ├── StackoverflowApi.java │ │ ├── questions │ │ │ ├── QuestionDetailsResponseSchema.java │ │ │ ├── QuestionSchema.java │ │ │ └── QuestionsListResponseSchema.java │ │ └── users │ │ │ └── UserSchema.java │ │ ├── questions │ │ ├── FetchLastActiveQuestionsUseCase.java │ │ ├── FetchQuestionDetailsUseCase.java │ │ ├── Question.java │ │ └── QuestionDetails.java │ │ └── screens │ │ ├── common │ │ ├── ViewMvcFactory.java │ │ ├── controllers │ │ │ ├── BackPressDispatcher.java │ │ │ ├── BackPressedListener.java │ │ │ ├── BaseActivity.java │ │ │ └── BaseFragment.java │ │ ├── dialogs │ │ │ ├── BaseDialog.java │ │ │ ├── DialogsEventBus.java │ │ │ ├── DialogsManager.java │ │ │ ├── infodialog │ │ │ │ └── InfoDialog.java │ │ │ └── promptdialog │ │ │ │ ├── PromptDialog.java │ │ │ │ ├── PromptDialogEvent.java │ │ │ │ ├── PromptViewMvc.java │ │ │ │ └── PromptViewMvcImpl.java │ │ ├── fragmentframehelper │ │ │ ├── FragmentFrameHelper.java │ │ │ ├── FragmentFrameWrapper.java │ │ │ └── HierarchicalFragment.java │ │ ├── main │ │ │ └── MainActivity.java │ │ ├── navdrawer │ │ │ ├── DrawerItems.java │ │ │ ├── NavDrawerHelper.java │ │ │ ├── NavDrawerViewMvc.java │ │ │ └── NavDrawerViewMvcImpl.java │ │ ├── screensnavigator │ │ │ └── ScreensNavigator.java │ │ ├── toastshelper │ │ │ └── ToastsHelper.java │ │ ├── toolbar │ │ │ └── ToolbarViewMvc.java │ │ └── views │ │ │ ├── BaseObservableViewMvc.java │ │ │ ├── BaseViewMvc.java │ │ │ ├── ObservableViewMvc.java │ │ │ └── ViewMvc.java │ │ ├── questiondetails │ │ ├── QuestionDetailsFragment.java │ │ ├── QuestionDetailsViewMvc.java │ │ └── QuestionDetailsViewMvcImpl.java │ │ └── questionslist │ │ ├── QuestionsListController.java │ │ ├── QuestionsListFragment.java │ │ ├── QuestionsListViewMvc.java │ │ ├── QuestionsListViewMvcImpl.java │ │ ├── QuestionsRecyclerAdapter.java │ │ └── questionslistitem │ │ ├── QuestionsListItemViewMvc.java │ │ └── QuestionsListItemViewMvcImpl.java │ └── res │ ├── drawable-hdpi │ ├── ic_arrow_back.png │ ├── ic_menu.png │ └── ic_view_list.png │ ├── drawable-mdpi │ ├── ic_arrow_back.png │ ├── ic_menu.png │ └── ic_view_list.png │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable-xhdpi │ ├── ic_arrow_back.png │ ├── ic_menu.png │ └── ic_view_list.png │ ├── drawable-xxhdpi │ ├── ic_arrow_back.png │ ├── ic_menu.png │ └── ic_view_list.png │ ├── drawable-xxxhdpi │ ├── ic_arrow_back.png │ ├── ic_menu.png │ └── ic_view_list.png │ ├── drawable │ ├── ic_launcher_background.xml │ └── ic_location.xml │ ├── layout │ ├── dialog_info.xml │ ├── dialog_prompt.xml │ ├── element_toolbar.xml │ ├── layout_content_frame.xml │ ├── layout_drawer.xml │ ├── layout_question_details.xml │ ├── layout_question_list_item.xml │ ├── layout_questions_list.xml │ └── layout_toolbar.xml │ ├── menu │ └── menu_drawer.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.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 │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #built application files 3 | *.apk 4 | *.ap_ 5 | 6 | # files for the dex VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # generated files 13 | bin/ 14 | gen/ 15 | 16 | # Local configuration file (sdk path, etc) 17 | local.properties 18 | 19 | # Windows thumbnail db 20 | Thumbs.db 21 | 22 | # OSX files 23 | .DS_Store 24 | 25 | # Android Studio 26 | .idea/ 27 | .gradle 28 | build/ 29 | *.iml 30 | captures/ 31 | .externalNativeBuild -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Architecture Masterclass 2 | 3 | Tutorial application for the old version of [my course about architecture of Android applications](https://go.techyourchance.com/android-architecture-course-github) 4 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 33 5 | defaultConfig { 6 | applicationId "com.techyourchance.mvc" 7 | minSdkVersion 19 8 | targetSdkVersion 33 9 | versionCode 1 10 | versionName "1.0" 11 | } 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 16 | } 17 | } 18 | } 19 | 20 | dependencies { 21 | implementation fileTree(dir: 'libs', include: ['*.jar']) 22 | 23 | implementation 'com.android.support:appcompat-v7:28.0.0' 24 | implementation 'com.android.support:design:28.0.0' 25 | implementation 'com.android.support.constraint:constraint-layout:2.0.4' 26 | 27 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 28 | implementation 'com.squareup.retrofit2:converter-gson:2.3.0' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/common/BaseObservable.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.common; 2 | 3 | import java.util.Collections; 4 | import java.util.Set; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | 7 | public abstract class BaseObservable { 8 | 9 | // thread-safe set of listeners 10 | private final Set mListeners = Collections.newSetFromMap( 11 | new ConcurrentHashMap(1)); 12 | 13 | 14 | public final void registerListener(LISTENER_CLASS listener) { 15 | mListeners.add(listener); 16 | } 17 | 18 | public final void unregisterListener(LISTENER_CLASS listener) { 19 | mListeners.remove(listener); 20 | } 21 | 22 | protected final Set getListeners() { 23 | return Collections.unmodifiableSet(mListeners); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/common/Constants.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.common; 2 | 3 | public final class Constants { 4 | private Constants() {} 5 | 6 | public static final int QUESTIONS_LIST_PAGE_SIZE = 20; 7 | 8 | public static final String BASE_URL = "http://api.stackexchange.com/2.2/"; 9 | 10 | public static final String STACKOVERFLOW_API_KEY = "f)yov8mEGrYZa1dJDb2gpg(("; 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/common/CustomApplication.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.common; 2 | 3 | import android.app.Application; 4 | 5 | import com.techyourchance.mvc.common.dependencyinjection.CompositionRoot; 6 | 7 | public class CustomApplication extends Application { 8 | 9 | private CompositionRoot mCompositionRoot; 10 | 11 | @Override 12 | public void onCreate() { 13 | super.onCreate(); 14 | mCompositionRoot = new CompositionRoot(); 15 | } 16 | 17 | public CompositionRoot getCompositionRoot() { 18 | return mCompositionRoot; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/common/dependencyinjection/ActivityCompositionRoot.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.common.dependencyinjection; 2 | 3 | import android.app.Activity; 4 | import android.support.v4.app.FragmentActivity; 5 | 6 | import com.techyourchance.mvc.common.permissions.PermissionsHelper; 7 | import com.techyourchance.mvc.networking.StackoverflowApi; 8 | import com.techyourchance.mvc.screens.common.dialogs.DialogsEventBus; 9 | 10 | public class ActivityCompositionRoot { 11 | 12 | private final CompositionRoot mCompositionRoot; 13 | private final FragmentActivity mActivity; 14 | 15 | private PermissionsHelper mPermissionsHelper; 16 | 17 | public ActivityCompositionRoot(CompositionRoot compositionRoot, FragmentActivity activity) { 18 | mCompositionRoot = compositionRoot; 19 | mActivity = activity; 20 | } 21 | 22 | public FragmentActivity getActivity() { 23 | return mActivity; 24 | } 25 | 26 | public StackoverflowApi getStackoverflowApi() { 27 | return mCompositionRoot.getStackoverflowApi(); 28 | } 29 | 30 | public DialogsEventBus getDialogsEventBus() { 31 | return mCompositionRoot.getDialogsEventBus(); 32 | } 33 | 34 | public PermissionsHelper getPermissionsHelper() { 35 | if (mPermissionsHelper == null) { 36 | mPermissionsHelper = new PermissionsHelper(getActivity()); 37 | } 38 | return mPermissionsHelper; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/common/dependencyinjection/CompositionRoot.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.common.dependencyinjection; 2 | 3 | import com.techyourchance.mvc.common.Constants; 4 | import com.techyourchance.mvc.networking.StackoverflowApi; 5 | import com.techyourchance.mvc.screens.common.dialogs.DialogsEventBus; 6 | 7 | import retrofit2.Retrofit; 8 | import retrofit2.converter.gson.GsonConverterFactory; 9 | 10 | public class CompositionRoot { 11 | 12 | private Retrofit mRetrofit; 13 | private DialogsEventBus mDialogsEventBus; 14 | 15 | private Retrofit getRetrofit() { 16 | if (mRetrofit == null) { 17 | mRetrofit = new Retrofit.Builder() 18 | .baseUrl(Constants.BASE_URL) 19 | .addConverterFactory(GsonConverterFactory.create()) 20 | .build(); 21 | } 22 | return mRetrofit; 23 | } 24 | 25 | public StackoverflowApi getStackoverflowApi() { 26 | return getRetrofit().create(StackoverflowApi.class); 27 | } 28 | 29 | public DialogsEventBus getDialogsEventBus() { 30 | if (mDialogsEventBus == null) { 31 | mDialogsEventBus = new DialogsEventBus(); 32 | } 33 | return mDialogsEventBus; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/common/dependencyinjection/ControllerCompositionRoot.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.common.dependencyinjection; 2 | 3 | import android.content.Context; 4 | import android.support.v4.app.FragmentActivity; 5 | import android.support.v4.app.FragmentManager; 6 | import android.view.LayoutInflater; 7 | 8 | import com.techyourchance.mvc.common.permissions.PermissionsHelper; 9 | import com.techyourchance.mvc.networking.StackoverflowApi; 10 | import com.techyourchance.mvc.questions.FetchLastActiveQuestionsUseCase; 11 | import com.techyourchance.mvc.questions.FetchQuestionDetailsUseCase; 12 | import com.techyourchance.mvc.screens.common.controllers.BackPressDispatcher; 13 | import com.techyourchance.mvc.screens.common.dialogs.DialogsEventBus; 14 | import com.techyourchance.mvc.screens.common.dialogs.DialogsManager; 15 | import com.techyourchance.mvc.screens.common.fragmentframehelper.FragmentFrameHelper; 16 | import com.techyourchance.mvc.screens.common.fragmentframehelper.FragmentFrameWrapper; 17 | import com.techyourchance.mvc.screens.common.navdrawer.NavDrawerHelper; 18 | import com.techyourchance.mvc.screens.common.toastshelper.ToastsHelper; 19 | import com.techyourchance.mvc.screens.common.screensnavigator.ScreensNavigator; 20 | import com.techyourchance.mvc.screens.common.ViewMvcFactory; 21 | import com.techyourchance.mvc.screens.questionslist.QuestionsListController; 22 | 23 | public class ControllerCompositionRoot { 24 | 25 | private final ActivityCompositionRoot mActivityCompositionRoot; 26 | 27 | public ControllerCompositionRoot(ActivityCompositionRoot activityCompositionRoot) { 28 | mActivityCompositionRoot = activityCompositionRoot; 29 | } 30 | 31 | private FragmentActivity getActivity() { 32 | return mActivityCompositionRoot.getActivity(); 33 | } 34 | 35 | private Context getContext() { 36 | return getActivity(); 37 | } 38 | 39 | private FragmentManager getFragmentManager() { 40 | return getActivity().getSupportFragmentManager(); 41 | } 42 | 43 | private StackoverflowApi getStackoverflowApi() { 44 | return mActivityCompositionRoot.getStackoverflowApi(); 45 | } 46 | 47 | private LayoutInflater getLayoutInflater() { 48 | return LayoutInflater.from(getContext()); 49 | } 50 | 51 | public ViewMvcFactory getViewMvcFactory() { 52 | return new ViewMvcFactory(getLayoutInflater(), getNavDrawerHelper()); 53 | } 54 | 55 | private NavDrawerHelper getNavDrawerHelper() { 56 | return (NavDrawerHelper) getActivity(); 57 | } 58 | 59 | public FetchQuestionDetailsUseCase getFetchQuestionDetailsUseCase() { 60 | return new FetchQuestionDetailsUseCase(getStackoverflowApi()); 61 | } 62 | 63 | public FetchLastActiveQuestionsUseCase getFetchLastActiveQuestionsUseCase() { 64 | return new FetchLastActiveQuestionsUseCase(getStackoverflowApi()); 65 | } 66 | 67 | public QuestionsListController getQuestionsListController() { 68 | return new QuestionsListController( 69 | getFetchLastActiveQuestionsUseCase(), 70 | getScreensNavigator(), 71 | getDialogsManager(), 72 | getDialogsEventBus() 73 | ); 74 | } 75 | 76 | public ToastsHelper getToastsHelper() { 77 | return new ToastsHelper(getContext()); 78 | } 79 | 80 | public ScreensNavigator getScreensNavigator() { 81 | return new ScreensNavigator(getFragmentFrameHelper()); 82 | } 83 | 84 | private FragmentFrameHelper getFragmentFrameHelper() { 85 | return new FragmentFrameHelper(getActivity(), getFragmentFrameWrapper(), getFragmentManager()); 86 | } 87 | 88 | private FragmentFrameWrapper getFragmentFrameWrapper() { 89 | return (FragmentFrameWrapper) getActivity(); 90 | } 91 | 92 | public BackPressDispatcher getBackPressDispatcher() { 93 | return (BackPressDispatcher) getActivity(); 94 | } 95 | 96 | public DialogsManager getDialogsManager() { 97 | return new DialogsManager(getContext(), getFragmentManager()); 98 | } 99 | 100 | public DialogsEventBus getDialogsEventBus() { 101 | return mActivityCompositionRoot.getDialogsEventBus(); 102 | } 103 | 104 | public PermissionsHelper getPermissionsHelper() { 105 | return mActivityCompositionRoot.getPermissionsHelper(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/common/permissions/PermissionsHelper.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.common.permissions; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.pm.PackageManager; 6 | import android.support.annotation.NonNull; 7 | import android.support.v4.app.ActivityCompat; 8 | import android.support.v4.content.ContextCompat; 9 | 10 | import com.techyourchance.mvc.common.BaseObservable; 11 | 12 | public class PermissionsHelper extends BaseObservable { 13 | 14 | public interface Listener { 15 | void onPermissionGranted(String permission, int requestCode); 16 | void onPermissionDeclined(String permission, int requestCode); 17 | void onPermissionDeclinedDontAskAgain(String permission, int requestCode); 18 | } 19 | 20 | private final Activity mActivity; 21 | 22 | public PermissionsHelper(Activity activity) { 23 | mActivity = activity; 24 | } 25 | 26 | public boolean hasPermission(String permission) { 27 | return ContextCompat.checkSelfPermission(mActivity, permission) == PackageManager.PERMISSION_GRANTED; 28 | } 29 | 30 | public void requestPermission(String permission, int requestCode) { 31 | ActivityCompat.requestPermissions(mActivity, new String[]{ permission }, requestCode); 32 | } 33 | 34 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 35 | if (permissions.length < 1) { 36 | throw new RuntimeException("no permissions on request result"); 37 | } 38 | String permission = permissions[0]; 39 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 40 | notifyPermissionGranted(permission, requestCode); 41 | } else { 42 | if (ActivityCompat.shouldShowRequestPermissionRationale(mActivity, permission)) { 43 | notifyPermissionDeclined(permission, requestCode); 44 | } else { 45 | notifyPermissionDeclinedDontAskAgain(permission, requestCode); 46 | } 47 | } 48 | } 49 | 50 | private void notifyPermissionDeclinedDontAskAgain(String permission, int requestCode) { 51 | for (Listener listener : getListeners()) { 52 | listener.onPermissionDeclinedDontAskAgain(permission, requestCode); 53 | } 54 | } 55 | 56 | private void notifyPermissionDeclined(String permission, int requestCode) { 57 | for (Listener listener : getListeners()) { 58 | listener.onPermissionDeclined(permission, requestCode); 59 | } 60 | } 61 | 62 | private void notifyPermissionGranted(String permission, int requestCode) { 63 | for (Listener listener : getListeners()) { 64 | listener.onPermissionGranted(permission, requestCode); 65 | } 66 | } 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/networking/StackoverflowApi.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.networking; 2 | 3 | import com.techyourchance.mvc.networking.questions.QuestionDetailsResponseSchema; 4 | import com.techyourchance.mvc.networking.questions.QuestionsListResponseSchema; 5 | import com.techyourchance.mvc.common.Constants; 6 | 7 | import retrofit2.Call; 8 | import retrofit2.http.GET; 9 | import retrofit2.http.Path; 10 | import retrofit2.http.Query; 11 | 12 | public interface StackoverflowApi { 13 | 14 | @GET("/questions?key=" + Constants.STACKOVERFLOW_API_KEY + "&sort=activity&order=desc&site=stackoverflow&filter=withbody") 15 | Call fetchLastActiveQuestions(@Query("pagesize") Integer pageSize); 16 | 17 | @GET("/questions/{questionId}?key=" + Constants.STACKOVERFLOW_API_KEY + "&site=stackoverflow&filter=withbody") 18 | Call fetchQuestionDetails(@Path("questionId") String questionId); 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/networking/questions/QuestionDetailsResponseSchema.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.networking.questions; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | public class QuestionDetailsResponseSchema { 9 | 10 | @SerializedName("items") 11 | private final List mQuestions; 12 | 13 | public QuestionDetailsResponseSchema(QuestionSchema question) { 14 | mQuestions = Collections.singletonList(question); 15 | } 16 | 17 | public QuestionSchema getQuestion() { 18 | return mQuestions.get(0); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/networking/questions/QuestionSchema.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.networking.questions; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import com.techyourchance.mvc.networking.users.UserSchema; 5 | 6 | public class QuestionSchema { 7 | 8 | @SerializedName("title") 9 | private final String mTitle; 10 | 11 | @SerializedName("question_id") 12 | private final String mId; 13 | 14 | @SerializedName("body") 15 | private final String mBody; 16 | 17 | @SerializedName("owner") 18 | private final UserSchema mOwner; 19 | 20 | public QuestionSchema(String title, String id, String body, UserSchema owner) { 21 | mTitle = title; 22 | mId = id; 23 | mBody = body; 24 | mOwner = owner; 25 | } 26 | 27 | public String getTitle() { 28 | return mTitle; 29 | } 30 | 31 | public String getId() { 32 | return mId; 33 | } 34 | 35 | public String getBody() { 36 | return mBody; 37 | } 38 | 39 | public UserSchema getOwner() { 40 | return mOwner; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/networking/questions/QuestionsListResponseSchema.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.networking.questions; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.util.List; 6 | 7 | public class QuestionsListResponseSchema { 8 | 9 | @SerializedName("items") 10 | private final List mQuestions; 11 | 12 | public QuestionsListResponseSchema(List questions) { 13 | mQuestions = questions; 14 | } 15 | 16 | public List getQuestions() { 17 | return mQuestions; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/networking/users/UserSchema.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.networking.users; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class UserSchema { 6 | 7 | @SerializedName("display_name") 8 | private final String mUserDisplayName; 9 | 10 | @SerializedName("profile_image") 11 | private final String mUserAvatarUrl; 12 | 13 | public UserSchema(String userDisplayName, String userAvatarUrl) { 14 | mUserDisplayName = userDisplayName; 15 | mUserAvatarUrl = userAvatarUrl; 16 | } 17 | 18 | public String getUserAvatarUrl() { 19 | return mUserAvatarUrl; 20 | } 21 | 22 | public String getUserDisplayName() { 23 | return mUserDisplayName; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/questions/FetchLastActiveQuestionsUseCase.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.questions; 2 | 3 | import com.techyourchance.mvc.common.BaseObservable; 4 | import com.techyourchance.mvc.common.Constants; 5 | import com.techyourchance.mvc.networking.questions.QuestionSchema; 6 | import com.techyourchance.mvc.networking.questions.QuestionsListResponseSchema; 7 | import com.techyourchance.mvc.networking.StackoverflowApi; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import retrofit2.Call; 13 | import retrofit2.Callback; 14 | import retrofit2.Response; 15 | 16 | public class FetchLastActiveQuestionsUseCase extends BaseObservable { 17 | 18 | public interface Listener { 19 | void onLastActiveQuestionsFetched(List questions); 20 | void onLastActiveQuestionsFetchFailed(); 21 | } 22 | 23 | private final StackoverflowApi mStackoverflowApi; 24 | 25 | public FetchLastActiveQuestionsUseCase(StackoverflowApi stackoverflowApi) { 26 | mStackoverflowApi = stackoverflowApi; 27 | } 28 | 29 | public void fetchLastActiveQuestionsAndNotify() { 30 | mStackoverflowApi.fetchLastActiveQuestions(Constants.QUESTIONS_LIST_PAGE_SIZE) 31 | .enqueue(new Callback() { 32 | @Override 33 | public void onResponse(Call call, Response response) { 34 | if (response.isSuccessful()) { 35 | notifySuccess(response.body().getQuestions()); 36 | } else { 37 | notifyFailure(); 38 | } 39 | } 40 | 41 | @Override 42 | public void onFailure(Call call, Throwable t) { 43 | notifyFailure(); 44 | } 45 | } ); 46 | } 47 | 48 | private void notifyFailure() { 49 | for (Listener listener : getListeners()) { 50 | listener.onLastActiveQuestionsFetchFailed(); 51 | } 52 | } 53 | 54 | private void notifySuccess(List questionSchemas) { 55 | List questions = new ArrayList<>(questionSchemas.size()); 56 | for (QuestionSchema questionSchema : questionSchemas) { 57 | questions.add(new Question(questionSchema.getId(), questionSchema.getTitle())); 58 | } 59 | for (Listener listener : getListeners()) { 60 | listener.onLastActiveQuestionsFetched(questions); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/questions/FetchQuestionDetailsUseCase.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.questions; 2 | 3 | import com.techyourchance.mvc.common.BaseObservable; 4 | import com.techyourchance.mvc.networking.questions.QuestionDetailsResponseSchema; 5 | import com.techyourchance.mvc.networking.questions.QuestionSchema; 6 | import com.techyourchance.mvc.networking.StackoverflowApi; 7 | 8 | import retrofit2.Call; 9 | import retrofit2.Callback; 10 | import retrofit2.Response; 11 | 12 | public class FetchQuestionDetailsUseCase extends BaseObservable { 13 | 14 | public interface Listener { 15 | void onQuestionDetailsFetched(QuestionDetails questionDetails); 16 | void onQuestionDetailsFetchFailed(); 17 | } 18 | 19 | private final StackoverflowApi mStackoverflowApi; 20 | 21 | public FetchQuestionDetailsUseCase(StackoverflowApi stackoverflowApi) { 22 | mStackoverflowApi = stackoverflowApi; 23 | } 24 | 25 | public void fetchQuestionDetailsAndNotify(String questionId) { 26 | mStackoverflowApi.fetchQuestionDetails(questionId) 27 | .enqueue(new Callback() { 28 | @Override 29 | public void onResponse(Call call, Response response) { 30 | if (response.isSuccessful()) { 31 | notifySuccess(response.body().getQuestion()); 32 | } else { 33 | notifyFailure(); 34 | } 35 | } 36 | 37 | @Override 38 | public void onFailure(Call call, Throwable t) { 39 | notifyFailure(); 40 | } 41 | }); 42 | } 43 | 44 | private void notifyFailure() { 45 | for (Listener listener : getListeners()) { 46 | listener.onQuestionDetailsFetchFailed(); 47 | } 48 | } 49 | 50 | private void notifySuccess(QuestionSchema questionSchema) { 51 | for (Listener listener : getListeners()) { 52 | listener.onQuestionDetailsFetched( 53 | new QuestionDetails( 54 | questionSchema.getId(), 55 | questionSchema.getTitle(), 56 | questionSchema.getBody() 57 | )); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/questions/Question.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.questions; 2 | 3 | public class Question { 4 | 5 | private final String mId; 6 | 7 | private final String mTitle; 8 | 9 | public Question(String id, String title) { 10 | mId = id; 11 | mTitle = title; 12 | } 13 | 14 | public String getId() { 15 | return mId; 16 | } 17 | 18 | public String getTitle() { 19 | return mTitle; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/questions/QuestionDetails.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.questions; 2 | 3 | public class QuestionDetails { 4 | 5 | private final String mId; 6 | 7 | private final String mTitle; 8 | 9 | private final String mBody; 10 | 11 | public QuestionDetails(String id, String title, String body) { 12 | mId = id; 13 | mTitle = title; 14 | mBody = body; 15 | } 16 | 17 | public String getId() { 18 | return mId; 19 | } 20 | 21 | public String getTitle() { 22 | return mTitle; 23 | } 24 | 25 | public String getBody() { 26 | return mBody; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/ViewMvcFactory.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common; 2 | 3 | import android.support.annotation.Nullable; 4 | import android.view.LayoutInflater; 5 | import android.view.ViewGroup; 6 | 7 | import com.techyourchance.mvc.screens.common.dialogs.promptdialog.PromptViewMvc; 8 | import com.techyourchance.mvc.screens.common.dialogs.promptdialog.PromptViewMvcImpl; 9 | import com.techyourchance.mvc.screens.common.navdrawer.NavDrawerHelper; 10 | import com.techyourchance.mvc.screens.common.navdrawer.NavDrawerViewMvc; 11 | import com.techyourchance.mvc.screens.common.navdrawer.NavDrawerViewMvcImpl; 12 | import com.techyourchance.mvc.screens.common.toolbar.ToolbarViewMvc; 13 | import com.techyourchance.mvc.screens.questiondetails.QuestionDetailsViewMvc; 14 | import com.techyourchance.mvc.screens.questiondetails.QuestionDetailsViewMvcImpl; 15 | import com.techyourchance.mvc.screens.questionslist.questionslistitem.QuestionsListItemViewMvc; 16 | import com.techyourchance.mvc.screens.questionslist.questionslistitem.QuestionsListItemViewMvcImpl; 17 | import com.techyourchance.mvc.screens.questionslist.QuestionsListViewMvc; 18 | import com.techyourchance.mvc.screens.questionslist.QuestionsListViewMvcImpl; 19 | 20 | public class ViewMvcFactory { 21 | 22 | private final LayoutInflater mLayoutInflater; 23 | private final NavDrawerHelper mNavDrawerHelper; 24 | 25 | public ViewMvcFactory(LayoutInflater layoutInflater, NavDrawerHelper navDrawerHelper) { 26 | mLayoutInflater = layoutInflater; 27 | mNavDrawerHelper = navDrawerHelper; 28 | } 29 | 30 | public QuestionsListViewMvc getQuestionsListViewMvc(@Nullable ViewGroup parent) { 31 | return new QuestionsListViewMvcImpl(mLayoutInflater, parent, mNavDrawerHelper, this); 32 | } 33 | 34 | public QuestionsListItemViewMvc getQuestionsListItemViewMvc(@Nullable ViewGroup parent) { 35 | return new QuestionsListItemViewMvcImpl(mLayoutInflater, parent); 36 | } 37 | 38 | public QuestionDetailsViewMvc getQuestionDetailsViewMvc(@Nullable ViewGroup parent) { 39 | return new QuestionDetailsViewMvcImpl(mLayoutInflater, parent, this); 40 | } 41 | 42 | public ToolbarViewMvc getToolbarViewMvc(@Nullable ViewGroup parent) { 43 | return new ToolbarViewMvc(mLayoutInflater, parent); 44 | } 45 | 46 | public NavDrawerViewMvc getNavDrawerViewMvc(@Nullable ViewGroup parent) { 47 | return new NavDrawerViewMvcImpl(mLayoutInflater, parent); 48 | } 49 | 50 | public PromptViewMvc getPromptViewMvc(@Nullable ViewGroup parent) { 51 | return new PromptViewMvcImpl(mLayoutInflater, parent); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/controllers/BackPressDispatcher.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.controllers; 2 | 3 | public interface BackPressDispatcher { 4 | void registerListener(BackPressedListener listener); 5 | void unregisterListener(BackPressedListener listener); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/controllers/BackPressedListener.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.controllers; 2 | 3 | public interface BackPressedListener { 4 | /** 5 | * 6 | * @return true if the listener handled the back press; false otherwise 7 | */ 8 | boolean onBackPressed(); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/controllers/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.controllers; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | 5 | import com.techyourchance.mvc.common.CustomApplication; 6 | import com.techyourchance.mvc.common.dependencyinjection.ActivityCompositionRoot; 7 | import com.techyourchance.mvc.common.dependencyinjection.ControllerCompositionRoot; 8 | 9 | public class BaseActivity extends AppCompatActivity { 10 | 11 | private ActivityCompositionRoot mActivityCompositionRoot; 12 | private ControllerCompositionRoot mControllerCompositionRoot; 13 | 14 | public ActivityCompositionRoot getActivityCompositionRoot() { 15 | if (mActivityCompositionRoot == null) { 16 | mActivityCompositionRoot = new ActivityCompositionRoot( 17 | ((CustomApplication) getApplication()).getCompositionRoot(), 18 | this 19 | ); 20 | } 21 | return mActivityCompositionRoot; 22 | } 23 | 24 | protected ControllerCompositionRoot getCompositionRoot() { 25 | if (mControllerCompositionRoot == null) { 26 | mControllerCompositionRoot = new ControllerCompositionRoot(getActivityCompositionRoot()); 27 | } 28 | return mControllerCompositionRoot; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/controllers/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.controllers; 2 | 3 | import android.support.v4.app.Fragment; 4 | 5 | import com.techyourchance.mvc.common.CustomApplication; 6 | import com.techyourchance.mvc.common.dependencyinjection.ControllerCompositionRoot; 7 | import com.techyourchance.mvc.screens.common.main.MainActivity; 8 | 9 | public class BaseFragment extends Fragment { 10 | 11 | private ControllerCompositionRoot mControllerCompositionRoot; 12 | 13 | protected ControllerCompositionRoot getCompositionRoot() { 14 | if (mControllerCompositionRoot == null) { 15 | mControllerCompositionRoot = new ControllerCompositionRoot( 16 | ((MainActivity) requireActivity()).getActivityCompositionRoot() 17 | ); 18 | } 19 | return mControllerCompositionRoot; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/dialogs/BaseDialog.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.dialogs; 2 | 3 | import android.support.v4.app.DialogFragment; 4 | 5 | import com.techyourchance.mvc.common.CustomApplication; 6 | import com.techyourchance.mvc.common.dependencyinjection.ControllerCompositionRoot; 7 | import com.techyourchance.mvc.screens.common.main.MainActivity; 8 | 9 | public abstract class BaseDialog extends DialogFragment { 10 | 11 | 12 | private ControllerCompositionRoot mControllerCompositionRoot; 13 | 14 | protected ControllerCompositionRoot getCompositionRoot() { 15 | if (mControllerCompositionRoot == null) { 16 | mControllerCompositionRoot = new ControllerCompositionRoot( 17 | ((MainActivity) requireActivity()).getActivityCompositionRoot() 18 | ); 19 | } 20 | return mControllerCompositionRoot; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/dialogs/DialogsEventBus.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.dialogs; 2 | 3 | import com.techyourchance.mvc.common.BaseObservable; 4 | 5 | public class DialogsEventBus extends BaseObservable { 6 | 7 | public interface Listener { 8 | void onDialogEvent(Object event); 9 | } 10 | 11 | public void postEvent(Object event) { 12 | for (Listener listener : getListeners()) { 13 | listener.onDialogEvent(event); 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/dialogs/DialogsManager.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.dialogs; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.DialogFragment; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v4.app.FragmentManager; 8 | 9 | import com.techyourchance.mvc.R; 10 | import com.techyourchance.mvc.screens.common.dialogs.infodialog.InfoDialog; 11 | import com.techyourchance.mvc.screens.common.dialogs.promptdialog.PromptDialog; 12 | 13 | public class DialogsManager { 14 | 15 | private final Context mContext; 16 | private final FragmentManager mFragmentManager; 17 | 18 | public DialogsManager(Context context, FragmentManager fragmentManager) { 19 | mContext = context; 20 | mFragmentManager = fragmentManager; 21 | } 22 | 23 | public void showUseCaseErrorDialog(@Nullable String tag) { 24 | DialogFragment dialogFragment = PromptDialog.newPromptDialog( 25 | getString(R.string.error_network_call_failed_title), 26 | getString(R.string.error_network_call_failed_message), 27 | getString(R.string.error_network_call_failed_positive_button_caption), 28 | getString(R.string.error_network_call_failed_negative_button_caption) 29 | ); 30 | dialogFragment.show(mFragmentManager, tag); 31 | } 32 | 33 | public void showPermissionGrantedDialog(@Nullable String tag) { 34 | DialogFragment dialogFragment = InfoDialog.newInfoDialog( 35 | getString(R.string.permission_dialog_title), 36 | getString(R.string.permission_dialog_granted_message), 37 | getString(R.string.permission_dialog_button_caption) 38 | ); 39 | dialogFragment.show(mFragmentManager, tag); 40 | } 41 | 42 | public void showPermissionDeclinedCantAskMoreDialog(@Nullable String tag) { 43 | DialogFragment dialogFragment = InfoDialog.newInfoDialog( 44 | getString(R.string.permission_dialog_title), 45 | getString(R.string.permission_dialog_cant_ask_more), 46 | getString(R.string.permission_dialog_button_caption) 47 | ); 48 | dialogFragment.show(mFragmentManager, tag); 49 | } 50 | 51 | public void showDeclinedDialog(@Nullable String tag) { 52 | DialogFragment dialogFragment = InfoDialog.newInfoDialog( 53 | getString(R.string.permission_dialog_title), 54 | getString(R.string.permission_dialog_user_declined), 55 | getString(R.string.permission_dialog_button_caption) 56 | ); 57 | dialogFragment.show(mFragmentManager, tag); 58 | } 59 | 60 | private String getString(int stringId) { 61 | return mContext.getString(stringId); 62 | } 63 | 64 | public @Nullable String getShownDialogTag() { 65 | for (Fragment fragment : mFragmentManager.getFragments()) { 66 | if (fragment instanceof BaseDialog) { 67 | return fragment.getTag(); 68 | } 69 | } 70 | return null; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/dialogs/infodialog/InfoDialog.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.dialogs.infodialog; 2 | 3 | import android.app.Dialog; 4 | import android.os.Bundle; 5 | import android.support.annotation.NonNull; 6 | import android.support.annotation.Nullable; 7 | import android.support.v7.widget.AppCompatButton; 8 | import android.view.View; 9 | import android.widget.TextView; 10 | 11 | import com.techyourchance.mvc.R; 12 | import com.techyourchance.mvc.screens.common.dialogs.BaseDialog; 13 | 14 | public class InfoDialog extends BaseDialog { 15 | 16 | protected static final String ARG_TITLE = "ARG_TITLE"; 17 | protected static final String ARG_MESSAGE = "ARG_MESSAGE"; 18 | protected static final String ARG_BUTTON_CAPTION = "ARG_BUTTON_CAPTION"; 19 | 20 | public static InfoDialog newInfoDialog(String title, String message, String buttonCaption) { 21 | InfoDialog infoDialog = new InfoDialog(); 22 | Bundle args = new Bundle(3); 23 | args.putString(ARG_TITLE, title); 24 | args.putString(ARG_MESSAGE, message); 25 | args.putString(ARG_BUTTON_CAPTION, buttonCaption); 26 | infoDialog.setArguments(args); 27 | return infoDialog; 28 | } 29 | 30 | private TextView mTxtTitle; 31 | private TextView mTxtMessage; 32 | private AppCompatButton mBtnPositive; 33 | 34 | @NonNull 35 | @Override 36 | public final Dialog onCreateDialog(Bundle savedInstanceState) { 37 | if (getArguments() == null) { 38 | throw new IllegalStateException("arguments mustn't be null"); 39 | } 40 | 41 | Dialog dialog = new Dialog(requireContext()); 42 | dialog.setContentView(R.layout.dialog_info); 43 | 44 | mTxtTitle = dialog.findViewById(R.id.txt_title); 45 | mTxtMessage = dialog.findViewById(R.id.txt_message); 46 | mBtnPositive = dialog.findViewById(R.id.btn_positive); 47 | 48 | mTxtTitle.setText(getArguments().getString(ARG_TITLE)); 49 | mTxtMessage.setText(getArguments().getString(ARG_MESSAGE)); 50 | mBtnPositive.setText(getArguments().getString(ARG_BUTTON_CAPTION)); 51 | 52 | mBtnPositive.setOnClickListener(new View.OnClickListener() { 53 | @Override 54 | public void onClick(View v) { 55 | onButtonClicked(); 56 | } 57 | }); 58 | 59 | return dialog; 60 | } 61 | 62 | protected void onButtonClicked() { 63 | dismiss(); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/dialogs/promptdialog/PromptDialog.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.dialogs.promptdialog; 2 | 3 | import android.app.Dialog; 4 | import android.os.Bundle; 5 | import android.support.annotation.NonNull; 6 | import android.support.annotation.Nullable; 7 | import android.support.v7.widget.AppCompatButton; 8 | import android.view.View; 9 | import android.widget.TextView; 10 | 11 | import com.techyourchance.mvc.R; 12 | import com.techyourchance.mvc.screens.common.dialogs.BaseDialog; 13 | import com.techyourchance.mvc.screens.common.dialogs.DialogsEventBus; 14 | 15 | public class PromptDialog extends BaseDialog implements PromptViewMvc.Listener { 16 | 17 | protected static final String ARG_TITLE = "ARG_TITLE"; 18 | protected static final String ARG_MESSAGE = "ARG_MESSAGE"; 19 | protected static final String ARG_POSITIVE_BUTTON_CAPTION = "ARG_POSITIVE_BUTTON_CAPTION"; 20 | protected static final String ARG_NEGATIVE_BUTTON_CAPTION = "ARG_NEGATIVE_BUTTON_CAPTION"; 21 | 22 | public static PromptDialog newPromptDialog(String title, String message, String positiveButtonCaption, String negativeButtonCaption) { 23 | PromptDialog promptDialog = new PromptDialog(); 24 | Bundle args = new Bundle(4); 25 | args.putString(ARG_TITLE, title); 26 | args.putString(ARG_MESSAGE, message); 27 | args.putString(ARG_POSITIVE_BUTTON_CAPTION, positiveButtonCaption); 28 | args.putString(ARG_NEGATIVE_BUTTON_CAPTION, negativeButtonCaption); 29 | promptDialog.setArguments(args); 30 | return promptDialog; 31 | } 32 | 33 | private DialogsEventBus mDialogsEventBus; 34 | private PromptViewMvc mViewMvc; 35 | 36 | @Override 37 | public void onCreate(@Nullable Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | mDialogsEventBus = getCompositionRoot().getDialogsEventBus(); 40 | } 41 | 42 | @NonNull 43 | @Override 44 | public final Dialog onCreateDialog(Bundle savedInstanceState) { 45 | if (getArguments() == null) { 46 | throw new IllegalStateException("arguments mustn't be null"); 47 | } 48 | 49 | mViewMvc = getCompositionRoot().getViewMvcFactory().getPromptViewMvc(null); 50 | 51 | mViewMvc.setTitle(getArguments().getString(ARG_TITLE)); 52 | mViewMvc.setMessage(getArguments().getString(ARG_MESSAGE)); 53 | mViewMvc.setPositiveButtonCaption(getArguments().getString(ARG_POSITIVE_BUTTON_CAPTION)); 54 | mViewMvc.setNegativeButtonCaption(getArguments().getString(ARG_NEGATIVE_BUTTON_CAPTION)); 55 | 56 | Dialog dialog = new Dialog(requireContext()); 57 | dialog.setContentView(mViewMvc.getRootView()); 58 | 59 | return dialog; 60 | } 61 | 62 | @Override 63 | public void onStart() { 64 | super.onStart(); 65 | mViewMvc.registerListener(this); 66 | } 67 | 68 | @Override 69 | public void onStop() { 70 | super.onStop(); 71 | mViewMvc.unregisterListener(this); 72 | } 73 | 74 | @Override 75 | public void onPositiveButtonClicked() { 76 | dismiss(); 77 | mDialogsEventBus.postEvent(new PromptDialogEvent(PromptDialogEvent.Button.POSITIVE)); 78 | } 79 | 80 | @Override 81 | public void onNegativeButtonClicked() { 82 | dismiss(); 83 | mDialogsEventBus.postEvent(new PromptDialogEvent(PromptDialogEvent.Button.NEGATIVE)); 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/dialogs/promptdialog/PromptDialogEvent.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.dialogs.promptdialog; 2 | 3 | public class PromptDialogEvent { 4 | 5 | public enum Button { 6 | POSITIVE, NEGATIVE 7 | } 8 | 9 | private final Button mClickedButton; 10 | 11 | public PromptDialogEvent(Button clickedButton) { 12 | mClickedButton = clickedButton; 13 | } 14 | 15 | public Button getClickedButton() { 16 | return mClickedButton; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/dialogs/promptdialog/PromptViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.dialogs.promptdialog; 2 | 3 | import com.techyourchance.mvc.screens.common.views.ObservableViewMvc; 4 | 5 | public interface PromptViewMvc extends ObservableViewMvc { 6 | 7 | public interface Listener { 8 | void onPositiveButtonClicked(); 9 | void onNegativeButtonClicked(); 10 | } 11 | 12 | void setTitle(String title); 13 | void setMessage(String message); 14 | void setPositiveButtonCaption(String caption); 15 | void setNegativeButtonCaption(String caption); 16 | } 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/dialogs/promptdialog/PromptViewMvcImpl.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.dialogs.promptdialog; 2 | 3 | import android.support.annotation.Nullable; 4 | import android.support.v7.widget.AppCompatButton; 5 | import android.support.v7.widget.LinearLayoutManager; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.support.v7.widget.Toolbar; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.ProgressBar; 12 | import android.widget.TextView; 13 | 14 | import com.techyourchance.mvc.R; 15 | import com.techyourchance.mvc.questions.Question; 16 | import com.techyourchance.mvc.screens.common.ViewMvcFactory; 17 | import com.techyourchance.mvc.screens.common.navdrawer.NavDrawerHelper; 18 | import com.techyourchance.mvc.screens.common.toolbar.ToolbarViewMvc; 19 | import com.techyourchance.mvc.screens.common.views.BaseObservableViewMvc; 20 | import com.techyourchance.mvc.screens.questionslist.QuestionsListViewMvc; 21 | import com.techyourchance.mvc.screens.questionslist.QuestionsRecyclerAdapter; 22 | 23 | import java.util.List; 24 | 25 | public class PromptViewMvcImpl extends BaseObservableViewMvc 26 | implements PromptViewMvc { 27 | 28 | private TextView mTxtTitle; 29 | private TextView mTxtMessage; 30 | private AppCompatButton mBtnPositive; 31 | private AppCompatButton mBtnNegative; 32 | 33 | 34 | public PromptViewMvcImpl(LayoutInflater inflater, 35 | @Nullable ViewGroup parent) { 36 | setRootView(inflater.inflate(R.layout.dialog_prompt, parent, false)); 37 | 38 | mTxtTitle = findViewById(R.id.txt_title); 39 | mTxtMessage = findViewById(R.id.txt_message); 40 | mBtnPositive = findViewById(R.id.btn_positive); 41 | mBtnNegative = findViewById(R.id.btn_negative); 42 | 43 | mBtnPositive.setOnClickListener(new View.OnClickListener() { 44 | @Override 45 | public void onClick(View v) { 46 | for (Listener listener : getListeners()) { 47 | listener.onPositiveButtonClicked(); 48 | }; 49 | } 50 | }); 51 | 52 | mBtnNegative.setOnClickListener(new View.OnClickListener() { 53 | @Override 54 | public void onClick(View v) { 55 | for (Listener listener : getListeners()) { 56 | listener.onNegativeButtonClicked(); 57 | } 58 | } 59 | }); 60 | 61 | } 62 | 63 | @Override 64 | public void setTitle(String title) { 65 | mTxtTitle.setText(title); 66 | } 67 | 68 | @Override 69 | public void setMessage(String message) { 70 | mTxtMessage.setText(message); 71 | } 72 | 73 | @Override 74 | public void setPositiveButtonCaption(String caption) { 75 | mBtnPositive.setText(caption); 76 | } 77 | 78 | @Override 79 | public void setNegativeButtonCaption(String caption) { 80 | mBtnNegative.setText(caption); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/fragmentframehelper/FragmentFrameHelper.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.fragmentframehelper; 2 | 3 | import android.app.Activity; 4 | import android.support.v4.app.Fragment; 5 | import android.support.v4.app.FragmentManager; 6 | import android.support.v4.app.FragmentTransaction; 7 | 8 | 9 | public class FragmentFrameHelper { 10 | 11 | private final Activity mActivity; 12 | private final FragmentFrameWrapper mFragmentFrameWrapper; 13 | private final FragmentManager mFragmentManager; 14 | 15 | public FragmentFrameHelper(Activity activity, FragmentFrameWrapper fragmentFrameWrapper, FragmentManager fragmentManager) { 16 | mActivity = activity; 17 | mFragmentFrameWrapper = fragmentFrameWrapper; 18 | mFragmentManager = fragmentManager; 19 | } 20 | 21 | public void replaceFragment(Fragment newFragment) { 22 | replaceFragment(newFragment, true, false); 23 | } 24 | 25 | public void replaceFragmentDontAddToBackstack(Fragment newFragment) { 26 | replaceFragment(newFragment, false, false); 27 | } 28 | 29 | public void replaceFragmentAndClearBackstack(Fragment newFragment) { 30 | replaceFragment(newFragment, false, true); 31 | } 32 | 33 | public void navigateUp() { 34 | 35 | // Some navigateUp calls can be "lost" if they happen after the state has been saved 36 | if (mFragmentManager.isStateSaved()) { 37 | return; 38 | } 39 | 40 | Fragment currentFragment = getCurrentFragment(); 41 | 42 | if (mFragmentManager.getBackStackEntryCount() > 0) { 43 | 44 | // In a normal world, just popping back stack would be sufficient, but since android 45 | // is not normal, a call to popBackStack can leave the popped fragment on screen. 46 | // Therefore, we start with manual removal of the current fragment. 47 | // Description of the issue can be found here: https://stackoverflow.com/q/45278497/2463035 48 | removeCurrentFragment(); 49 | 50 | if (mFragmentManager.popBackStackImmediate()) { 51 | return; // navigated "up" in fragments back-stack 52 | } 53 | } 54 | 55 | if (HierarchicalFragment.class.isInstance(currentFragment)) { 56 | Fragment parentFragment = 57 | ((HierarchicalFragment)currentFragment).getHierarchicalParentFragment(); 58 | if (parentFragment != null) { 59 | replaceFragment(parentFragment, false, true); 60 | return; // navigate "up" to hierarchical parent fragment 61 | } 62 | } 63 | 64 | if (mActivity.onNavigateUp()) { 65 | return; // navigated "up" to hierarchical parent activity 66 | } 67 | 68 | mActivity.onBackPressed(); // no "up" navigation targets - just treat UP as back press 69 | } 70 | 71 | private Fragment getCurrentFragment() { 72 | return mFragmentManager.findFragmentById(getFragmentFrameId()); 73 | } 74 | 75 | private void replaceFragment(Fragment newFragment, boolean addToBackStack, boolean clearBackStack) { 76 | if (clearBackStack) { 77 | if (mFragmentManager.isStateSaved()) { 78 | // If the state is saved we can't clear the back stack. Simply not doing this, but 79 | // still replacing fragment is a bad idea. Therefore we abort the entire operation. 80 | return; 81 | } 82 | // Remove all entries from back stack 83 | mFragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); 84 | } 85 | 86 | FragmentTransaction ft = mFragmentManager.beginTransaction(); 87 | 88 | if (addToBackStack) { 89 | ft.addToBackStack(null); 90 | } 91 | 92 | // Change to a new fragment 93 | ft.replace(getFragmentFrameId(), newFragment, null); 94 | 95 | if (mFragmentManager.isStateSaved()) { 96 | // We acknowledge the possibility of losing this transaction if the app undergoes 97 | // save&restore flow after it is committed. 98 | ft.commitAllowingStateLoss(); 99 | } else { 100 | ft.commit(); 101 | } 102 | } 103 | 104 | private void removeCurrentFragment() { 105 | FragmentTransaction ft = mFragmentManager.beginTransaction(); 106 | ft.remove(getCurrentFragment()); 107 | ft.commit(); 108 | 109 | // not sure it is needed; will keep it as a reminder to myself if there will be problems 110 | // mFragmentManager.executePendingTransactions(); 111 | } 112 | 113 | private int getFragmentFrameId() { 114 | return mFragmentFrameWrapper.getFragmentFrame().getId(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/fragmentframehelper/FragmentFrameWrapper.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.fragmentframehelper; 2 | 3 | import android.widget.FrameLayout; 4 | 5 | public interface FragmentFrameWrapper { 6 | 7 | FrameLayout getFragmentFrame(); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/fragmentframehelper/HierarchicalFragment.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.fragmentframehelper; 2 | 3 | import android.support.annotation.Nullable; 4 | import android.support.v4.app.Fragment; 5 | 6 | public interface HierarchicalFragment { 7 | /** 8 | * In case of UP navigation when Fragments back-stack is empty, the Fragment returned by this 9 | * method will be navigated to. If this method returns null, then UP navigation will be 10 | * delegated to enclosing Activity. 11 | * @return hierarchical parent Fragment of this Fragment; null this Fragment has no hierarchical 12 | * parent 13 | */ 14 | @Nullable 15 | Fragment getHierarchicalParentFragment(); 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/main/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.main; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.annotation.NonNull; 7 | import android.support.annotation.Nullable; 8 | import android.support.v4.app.FragmentTransaction; 9 | import android.widget.FrameLayout; 10 | 11 | import com.techyourchance.mvc.R; 12 | import com.techyourchance.mvc.common.permissions.PermissionsHelper; 13 | import com.techyourchance.mvc.screens.common.controllers.BackPressDispatcher; 14 | import com.techyourchance.mvc.screens.common.controllers.BackPressedListener; 15 | import com.techyourchance.mvc.screens.common.controllers.BaseActivity; 16 | import com.techyourchance.mvc.screens.common.fragmentframehelper.FragmentFrameWrapper; 17 | import com.techyourchance.mvc.screens.common.navdrawer.NavDrawerHelper; 18 | import com.techyourchance.mvc.screens.common.navdrawer.NavDrawerViewMvc; 19 | import com.techyourchance.mvc.screens.common.screensnavigator.ScreensNavigator; 20 | import com.techyourchance.mvc.screens.questionslist.QuestionsListFragment; 21 | 22 | import java.util.HashSet; 23 | import java.util.Set; 24 | 25 | public class MainActivity extends BaseActivity implements 26 | BackPressDispatcher, 27 | FragmentFrameWrapper, 28 | NavDrawerViewMvc.Listener, 29 | NavDrawerHelper { 30 | 31 | private final Set mBackPressedListeners = new HashSet<>(); 32 | private ScreensNavigator mScreensNavigator; 33 | private PermissionsHelper mPermissionsHelper; 34 | 35 | private NavDrawerViewMvc mViewMvc; 36 | 37 | @Override 38 | protected void onCreate(@Nullable Bundle savedInstanceState) { 39 | super.onCreate(savedInstanceState); 40 | mScreensNavigator = getCompositionRoot().getScreensNavigator(); 41 | mPermissionsHelper = getCompositionRoot().getPermissionsHelper(); 42 | mViewMvc = getCompositionRoot().getViewMvcFactory().getNavDrawerViewMvc(null); 43 | setContentView(mViewMvc.getRootView()); 44 | 45 | if (savedInstanceState == null) { 46 | mScreensNavigator.toQuestionsList(); 47 | } 48 | } 49 | 50 | @Override 51 | protected void onStart() { 52 | super.onStart(); 53 | mViewMvc.registerListener(this); 54 | } 55 | 56 | @Override 57 | protected void onStop() { 58 | super.onStop(); 59 | mViewMvc.unregisterListener(this); 60 | } 61 | 62 | @Override 63 | public void onQuestionsListClicked() { 64 | mScreensNavigator.toQuestionsList(); 65 | } 66 | 67 | @Override 68 | public void registerListener(BackPressedListener listener) { 69 | mBackPressedListeners.add(listener); 70 | } 71 | 72 | @Override 73 | public void unregisterListener(BackPressedListener listener) { 74 | mBackPressedListeners.remove(listener); 75 | } 76 | 77 | @Override 78 | public void onBackPressed() { 79 | boolean isBackPressConsumedByAnyListener = false; 80 | for (BackPressedListener listener : mBackPressedListeners) { 81 | if (listener.onBackPressed()) { 82 | isBackPressConsumedByAnyListener = true; 83 | } 84 | } 85 | if (isBackPressConsumedByAnyListener) { 86 | return; 87 | } 88 | 89 | if (mViewMvc.isDrawerOpen()) { 90 | mViewMvc.closeDrawer(); 91 | } else { 92 | super.onBackPressed(); 93 | } 94 | } 95 | 96 | @Override 97 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 98 | mPermissionsHelper.onRequestPermissionsResult(requestCode, permissions, grantResults); 99 | } 100 | 101 | @Override 102 | public FrameLayout getFragmentFrame() { 103 | return mViewMvc.getFragmentFrame(); 104 | } 105 | 106 | @Override 107 | public void openDrawer() { 108 | mViewMvc.openDrawer(); 109 | } 110 | 111 | @Override 112 | public void closeDrawer() { 113 | mViewMvc.closeDrawer(); 114 | } 115 | 116 | @Override 117 | public boolean isDrawerOpen() { 118 | return mViewMvc.isDrawerOpen(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/navdrawer/DrawerItems.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.navdrawer; 2 | 3 | public enum DrawerItems { 4 | QUESTIONS_LIST 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/navdrawer/NavDrawerHelper.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.navdrawer; 2 | 3 | public interface NavDrawerHelper { 4 | 5 | void openDrawer(); 6 | void closeDrawer(); 7 | boolean isDrawerOpen(); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/navdrawer/NavDrawerViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.navdrawer; 2 | 3 | import android.widget.FrameLayout; 4 | 5 | import com.techyourchance.mvc.screens.common.views.ObservableViewMvc; 6 | 7 | public interface NavDrawerViewMvc extends ObservableViewMvc { 8 | 9 | interface Listener { 10 | 11 | void onQuestionsListClicked(); 12 | } 13 | 14 | FrameLayout getFragmentFrame(); 15 | 16 | boolean isDrawerOpen(); 17 | void openDrawer(); 18 | void closeDrawer(); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/navdrawer/NavDrawerViewMvcImpl.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.navdrawer; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | import android.support.design.widget.NavigationView; 6 | import android.support.v4.widget.DrawerLayout; 7 | import android.view.Gravity; 8 | import android.view.LayoutInflater; 9 | import android.view.MenuItem; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.FrameLayout; 13 | 14 | import com.techyourchance.mvc.R; 15 | import com.techyourchance.mvc.screens.common.views.BaseObservableViewMvc; 16 | 17 | public class NavDrawerViewMvcImpl extends BaseObservableViewMvc 18 | implements NavDrawerViewMvc { 19 | 20 | private final DrawerLayout mDrawerLayout; 21 | private final FrameLayout mFrameLayout; 22 | private final NavigationView mNavigationView; 23 | 24 | public NavDrawerViewMvcImpl(LayoutInflater inflater, @Nullable ViewGroup parent) { 25 | setRootView(inflater.inflate(R.layout.layout_drawer, parent, false)); 26 | mDrawerLayout = findViewById(R.id.drawer_layout); 27 | mFrameLayout = findViewById(R.id.frame_content); 28 | mNavigationView = findViewById(R.id.nav_view); 29 | 30 | mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { 31 | @Override 32 | public boolean onNavigationItemSelected(@NonNull MenuItem item) { 33 | mDrawerLayout.closeDrawers(); 34 | if (item.getItemId() == R.id.drawer_menu_questions_list) { 35 | for (Listener listener : getListeners()) { 36 | listener.onQuestionsListClicked(); 37 | } 38 | } 39 | return false; 40 | } 41 | }); 42 | } 43 | 44 | @Override 45 | public void openDrawer() { 46 | mDrawerLayout.openDrawer(Gravity.START); 47 | } 48 | 49 | @Override 50 | public boolean isDrawerOpen() { 51 | return mDrawerLayout.isDrawerOpen(Gravity.START); 52 | } 53 | 54 | @Override 55 | public void closeDrawer() { 56 | mDrawerLayout.closeDrawers(); 57 | } 58 | 59 | @Override 60 | public FrameLayout getFragmentFrame() { 61 | return mFrameLayout; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/screensnavigator/ScreensNavigator.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.screensnavigator; 2 | 3 | import android.support.v4.app.FragmentManager; 4 | import android.support.v4.app.FragmentTransaction; 5 | 6 | import com.techyourchance.mvc.screens.common.fragmentframehelper.FragmentFrameHelper; 7 | import com.techyourchance.mvc.screens.common.fragmentframehelper.FragmentFrameWrapper; 8 | import com.techyourchance.mvc.screens.questiondetails.QuestionDetailsFragment; 9 | import com.techyourchance.mvc.screens.questionslist.QuestionsListFragment; 10 | 11 | public class ScreensNavigator { 12 | 13 | private FragmentFrameHelper mFragmentFrameHelper; 14 | 15 | public ScreensNavigator(FragmentFrameHelper fragmentFrameHelper) { 16 | mFragmentFrameHelper = fragmentFrameHelper; 17 | } 18 | 19 | public void toQuestionDetails(String questionId) { 20 | mFragmentFrameHelper.replaceFragment(QuestionDetailsFragment.newInstance(questionId)); 21 | } 22 | 23 | public void toQuestionsList() { 24 | mFragmentFrameHelper.replaceFragmentAndClearBackstack(QuestionsListFragment.newInstance()); 25 | } 26 | 27 | public void navigateUp() { 28 | mFragmentFrameHelper.navigateUp(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/toastshelper/ToastsHelper.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.toastshelper; 2 | 3 | import android.content.Context; 4 | import android.widget.Toast; 5 | 6 | import com.techyourchance.mvc.R; 7 | 8 | public class ToastsHelper { 9 | 10 | private final Context mContext; 11 | 12 | public ToastsHelper(Context context) { 13 | mContext = context; 14 | } 15 | 16 | public void showUseCaseError() { 17 | Toast.makeText(mContext, R.string.error_network_call_failed, Toast.LENGTH_SHORT).show(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/toolbar/ToolbarViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.toolbar; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.ImageButton; 7 | import android.widget.TextView; 8 | 9 | import com.techyourchance.mvc.R; 10 | import com.techyourchance.mvc.screens.common.views.BaseViewMvc; 11 | import com.techyourchance.mvc.screens.questiondetails.QuestionDetailsViewMvcImpl; 12 | 13 | public class ToolbarViewMvc extends BaseViewMvc { 14 | 15 | public interface NavigateUpClickListener { 16 | void onNavigateUpClicked(); 17 | } 18 | 19 | public interface HamburgerClickListener { 20 | void onHamburgerClicked(); 21 | } 22 | 23 | public interface LocationRequestListener { 24 | void onLocationRequestClicked(); 25 | } 26 | 27 | private final TextView mTxtTitle; 28 | private final ImageButton mBtnBack; 29 | private final ImageButton mBtnHamburger; 30 | private final ImageButton mBtnLocationRequest; 31 | 32 | private NavigateUpClickListener mNavigateUpClickListener; 33 | private HamburgerClickListener mHamburgerClickListener; 34 | private LocationRequestListener mLocationRequestListener; 35 | 36 | public ToolbarViewMvc(LayoutInflater inflater, ViewGroup parent) { 37 | setRootView(inflater.inflate(R.layout.layout_toolbar, parent, false)); 38 | mTxtTitle = findViewById(R.id.txt_toolbar_title); 39 | mBtnHamburger = findViewById(R.id.btn_hamburger); 40 | mBtnHamburger.setOnClickListener(new View.OnClickListener() { 41 | @Override 42 | public void onClick(View view) { 43 | mHamburgerClickListener.onHamburgerClicked(); 44 | } 45 | }); 46 | mBtnBack = findViewById(R.id.btn_back); 47 | mBtnBack.setOnClickListener(new View.OnClickListener() { 48 | @Override 49 | public void onClick(View view) { 50 | mNavigateUpClickListener.onNavigateUpClicked(); 51 | } 52 | }); 53 | mBtnLocationRequest = findViewById(R.id.btn_location); 54 | mBtnLocationRequest.setOnClickListener(new View.OnClickListener() { 55 | @Override 56 | public void onClick(View view) { 57 | mLocationRequestListener.onLocationRequestClicked(); 58 | } 59 | }); 60 | } 61 | 62 | public void setTitle(String title) { 63 | mTxtTitle.setText(title); 64 | } 65 | 66 | public void enableHamburgerButtonAndListen(HamburgerClickListener hamburgerClickListener) { 67 | if (mNavigateUpClickListener != null) { 68 | throw new RuntimeException("hamburger and up shouldn't be shown together"); 69 | } 70 | mHamburgerClickListener = hamburgerClickListener; 71 | mBtnHamburger.setVisibility(View.VISIBLE); 72 | } 73 | 74 | public void enableUpButtonAndListen(NavigateUpClickListener navigateUpClickListener) { 75 | if (mHamburgerClickListener != null) { 76 | throw new RuntimeException("hamburger and up shouldn't be shown together"); 77 | } 78 | mNavigateUpClickListener = navigateUpClickListener; 79 | mBtnBack.setVisibility(View.VISIBLE); 80 | } 81 | 82 | public void enableLocationRequestButtonAndListen(LocationRequestListener locationRequestListener) { 83 | mLocationRequestListener = locationRequestListener; 84 | mBtnLocationRequest.setVisibility(View.VISIBLE); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/views/BaseObservableViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.views; 2 | 3 | import java.util.Collections; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | public abstract class BaseObservableViewMvc extends BaseViewMvc 8 | implements ObservableViewMvc { 9 | 10 | private Set mListeners = new HashSet<>(); 11 | 12 | @Override 13 | public final void registerListener(ListenerType listener) { 14 | mListeners.add(listener); 15 | } 16 | 17 | @Override 18 | public final void unregisterListener(ListenerType listener) { 19 | mListeners.remove(listener); 20 | } 21 | 22 | protected final Set getListeners() { 23 | return Collections.unmodifiableSet(mListeners); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/views/BaseViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.views; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.StringRes; 5 | import android.view.View; 6 | 7 | public abstract class BaseViewMvc implements ViewMvc { 8 | 9 | private View mRootView; 10 | 11 | @Override 12 | public View getRootView() { 13 | return mRootView; 14 | } 15 | 16 | protected void setRootView(View rootView) { 17 | mRootView = rootView; 18 | } 19 | 20 | protected T findViewById(int id) { 21 | return getRootView().findViewById(id); 22 | } 23 | 24 | protected Context getContext() { 25 | return getRootView().getContext(); 26 | } 27 | 28 | protected String getString(@StringRes int id) { 29 | return getContext().getString(id); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/views/ObservableViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.views; 2 | 3 | public interface ObservableViewMvc extends ViewMvc { 4 | 5 | void registerListener(ListenerType listener); 6 | 7 | void unregisterListener(ListenerType listener); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/common/views/ViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.common.views; 2 | 3 | import android.view.View; 4 | 5 | public interface ViewMvc { 6 | View getRootView(); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/questiondetails/QuestionDetailsFragment.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.questiondetails; 2 | 3 | import android.Manifest; 4 | import android.content.pm.PackageManager; 5 | import android.os.Bundle; 6 | import android.support.annotation.NonNull; 7 | import android.support.annotation.Nullable; 8 | import android.support.v4.app.ActivityCompat; 9 | import android.support.v4.content.ContextCompat; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | 14 | import com.techyourchance.mvc.common.permissions.PermissionsHelper; 15 | import com.techyourchance.mvc.questions.FetchQuestionDetailsUseCase; 16 | import com.techyourchance.mvc.questions.QuestionDetails; 17 | import com.techyourchance.mvc.screens.common.controllers.BaseFragment; 18 | import com.techyourchance.mvc.screens.common.dialogs.DialogsEventBus; 19 | import com.techyourchance.mvc.screens.common.dialogs.DialogsManager; 20 | import com.techyourchance.mvc.screens.common.dialogs.promptdialog.PromptDialogEvent; 21 | import com.techyourchance.mvc.screens.common.screensnavigator.ScreensNavigator; 22 | 23 | public class QuestionDetailsFragment extends BaseFragment implements 24 | FetchQuestionDetailsUseCase.Listener, 25 | QuestionDetailsViewMvc.Listener, 26 | DialogsEventBus.Listener, 27 | PermissionsHelper.Listener { 28 | 29 | private static final String ARG_QUESTION_ID = "ARG_QUESTION_ID"; 30 | 31 | private static final String DIALOG_ID_NETWORK_ERROR = "DIALOG_ID_NETWORK_ERROR"; 32 | 33 | private static final String SAVED_STATE_SCREEN_STATE = "SAVED_STATE_SCREEN_STATE"; 34 | public static final int REQUEST_CODE = 1001; 35 | 36 | public static QuestionDetailsFragment newInstance(String questionId) { 37 | Bundle args = new Bundle(); 38 | args.putString(ARG_QUESTION_ID, questionId); 39 | QuestionDetailsFragment fragment = new QuestionDetailsFragment(); 40 | fragment.setArguments(args); 41 | return fragment; 42 | } 43 | 44 | private enum ScreenState { 45 | IDLE, QUESTION_DETAILS_SHOWN, NETWORK_ERROR 46 | } 47 | 48 | private FetchQuestionDetailsUseCase mFetchQuestionDetailsUseCase; 49 | private ScreensNavigator mScreensNavigator; 50 | private DialogsManager mDialogsManager; 51 | private DialogsEventBus mDialogsEventBus; 52 | private PermissionsHelper mPermissionsHelper; 53 | 54 | private QuestionDetailsViewMvc mViewMvc; 55 | 56 | private ScreenState mScreenState = ScreenState.IDLE; 57 | 58 | @Override 59 | public void onCreate(@Nullable Bundle savedInstanceState) { 60 | super.onCreate(savedInstanceState); 61 | if (savedInstanceState != null) { 62 | mScreenState = (ScreenState) savedInstanceState.getSerializable(SAVED_STATE_SCREEN_STATE); 63 | } 64 | mPermissionsHelper = getCompositionRoot().getPermissionsHelper(); 65 | mFetchQuestionDetailsUseCase = getCompositionRoot().getFetchQuestionDetailsUseCase(); 66 | mScreensNavigator = getCompositionRoot().getScreensNavigator(); 67 | mDialogsManager = getCompositionRoot().getDialogsManager(); 68 | mDialogsEventBus = getCompositionRoot().getDialogsEventBus(); 69 | } 70 | 71 | @Nullable 72 | @Override 73 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 74 | mViewMvc = getCompositionRoot().getViewMvcFactory().getQuestionDetailsViewMvc(container); 75 | return mViewMvc.getRootView(); 76 | } 77 | 78 | @Override 79 | public void onStart() { 80 | super.onStart(); 81 | mFetchQuestionDetailsUseCase.registerListener(this); 82 | mViewMvc.registerListener(this); 83 | mDialogsEventBus.registerListener(this); 84 | mPermissionsHelper.registerListener(this); 85 | 86 | if (mScreenState != ScreenState.NETWORK_ERROR) { 87 | fetchQuestionDetailsAndNotify(); 88 | } 89 | } 90 | 91 | @Override 92 | public void onStop() { 93 | super.onStop(); 94 | mFetchQuestionDetailsUseCase.unregisterListener(this); 95 | mViewMvc.unregisterListener(this); 96 | mDialogsEventBus.unregisterListener(this); 97 | mPermissionsHelper.unregisterListener(this); 98 | } 99 | 100 | @Override 101 | public void onSaveInstanceState(@NonNull Bundle outState) { 102 | super.onSaveInstanceState(outState); 103 | outState.putSerializable(SAVED_STATE_SCREEN_STATE, mScreenState); 104 | } 105 | 106 | private void fetchQuestionDetailsAndNotify() { 107 | mViewMvc.showProgressIndication(); 108 | mFetchQuestionDetailsUseCase.fetchQuestionDetailsAndNotify(getQuestionId()); 109 | } 110 | 111 | private String getQuestionId() { 112 | return getArguments().getString(ARG_QUESTION_ID); 113 | } 114 | 115 | @Override 116 | public void onQuestionDetailsFetched(QuestionDetails questionDetails) { 117 | mScreenState = ScreenState.QUESTION_DETAILS_SHOWN; 118 | mViewMvc.hideProgressIndication(); 119 | mViewMvc.bindQuestion(questionDetails); 120 | } 121 | 122 | @Override 123 | public void onQuestionDetailsFetchFailed() { 124 | mScreenState = ScreenState.NETWORK_ERROR; 125 | mViewMvc.hideProgressIndication(); 126 | mDialogsManager.showUseCaseErrorDialog(DIALOG_ID_NETWORK_ERROR); 127 | } 128 | 129 | @Override 130 | public void onDialogEvent(Object event) { 131 | if (event instanceof PromptDialogEvent) { 132 | switch (((PromptDialogEvent) event).getClickedButton()) { 133 | case POSITIVE: 134 | mScreenState = ScreenState.IDLE; 135 | fetchQuestionDetailsAndNotify(); 136 | break; 137 | case NEGATIVE: 138 | mScreenState = ScreenState.IDLE; 139 | break; 140 | } 141 | } 142 | } 143 | 144 | @Override 145 | public void onLocationRequestClicked() { 146 | if (mPermissionsHelper.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) { 147 | mDialogsManager.showPermissionGrantedDialog(null); 148 | } else { 149 | mPermissionsHelper.requestPermission(Manifest.permission.ACCESS_FINE_LOCATION, REQUEST_CODE); 150 | } 151 | } 152 | 153 | @Override 154 | public void onPermissionGranted(String permission, int requestCode) { 155 | if (requestCode == REQUEST_CODE) { 156 | mDialogsManager.showPermissionGrantedDialog(null); 157 | } 158 | } 159 | 160 | @Override 161 | public void onPermissionDeclined(String permission, int requestCode) { 162 | if (requestCode == REQUEST_CODE) { 163 | mDialogsManager.showDeclinedDialog(null); 164 | } 165 | } 166 | 167 | @Override 168 | public void onPermissionDeclinedDontAskAgain(String permission, int requestCode) { 169 | if (requestCode == REQUEST_CODE) { 170 | mDialogsManager.showPermissionDeclinedCantAskMoreDialog(null); 171 | } 172 | } 173 | 174 | @Override 175 | public void onNavigateUpClicked() { 176 | mScreensNavigator.navigateUp(); 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/questiondetails/QuestionDetailsViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.questiondetails; 2 | 3 | import com.techyourchance.mvc.questions.QuestionDetails; 4 | import com.techyourchance.mvc.screens.common.navdrawer.DrawerItems; 5 | import com.techyourchance.mvc.screens.common.navdrawer.NavDrawerViewMvc; 6 | import com.techyourchance.mvc.screens.common.views.ObservableViewMvc; 7 | import com.techyourchance.mvc.screens.common.views.ViewMvc; 8 | 9 | public interface QuestionDetailsViewMvc extends ObservableViewMvc { 10 | 11 | public interface Listener { 12 | void onNavigateUpClicked(); 13 | void onLocationRequestClicked(); 14 | } 15 | 16 | void bindQuestion(QuestionDetails question); 17 | 18 | void showProgressIndication(); 19 | 20 | void hideProgressIndication(); 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/questiondetails/QuestionDetailsViewMvcImpl.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.questiondetails; 2 | 3 | import android.support.v7.widget.Toolbar; 4 | import android.text.Html; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ProgressBar; 9 | import android.widget.TextView; 10 | 11 | import com.techyourchance.mvc.R; 12 | import com.techyourchance.mvc.questions.QuestionDetails; 13 | import com.techyourchance.mvc.screens.common.ViewMvcFactory; 14 | import com.techyourchance.mvc.screens.common.toolbar.ToolbarViewMvc; 15 | import com.techyourchance.mvc.screens.common.views.BaseObservableViewMvc; 16 | 17 | 18 | public class QuestionDetailsViewMvcImpl extends BaseObservableViewMvc 19 | implements QuestionDetailsViewMvc { 20 | 21 | 22 | private final ToolbarViewMvc mToolbarViewMvc; 23 | private final Toolbar mToolbar; 24 | 25 | private final TextView mTxtQuestionTitle; 26 | private final TextView mTxtQuestionBody; 27 | private final ProgressBar mProgressBar; 28 | 29 | public QuestionDetailsViewMvcImpl(LayoutInflater inflater, ViewGroup parent, ViewMvcFactory viewMvcFactory) { 30 | 31 | setRootView(inflater.inflate(R.layout.layout_question_details, parent, false)); 32 | 33 | mTxtQuestionTitle = findViewById(R.id.txt_question_title); 34 | mTxtQuestionBody = findViewById(R.id.txt_question_body); 35 | mProgressBar = findViewById(R.id.progress); 36 | mToolbar = findViewById(R.id.toolbar); 37 | 38 | mToolbarViewMvc = viewMvcFactory.getToolbarViewMvc(mToolbar); 39 | 40 | initToolbar(); 41 | } 42 | 43 | private void initToolbar() { 44 | mToolbar.addView(mToolbarViewMvc.getRootView()); 45 | 46 | mToolbarViewMvc.setTitle(getString(R.string.question_details_screen_title)); 47 | 48 | mToolbarViewMvc.enableUpButtonAndListen(new ToolbarViewMvc.NavigateUpClickListener() { 49 | @Override 50 | public void onNavigateUpClicked() { 51 | for (Listener listener : getListeners()) { 52 | listener.onNavigateUpClicked(); 53 | } 54 | } 55 | }); 56 | 57 | mToolbarViewMvc.enableLocationRequestButtonAndListen(new ToolbarViewMvc.LocationRequestListener() { 58 | @Override 59 | public void onLocationRequestClicked() { 60 | for (Listener listener : getListeners()) { 61 | listener.onLocationRequestClicked(); 62 | } 63 | } 64 | }); 65 | } 66 | 67 | @Override 68 | public void bindQuestion(QuestionDetails question) { 69 | String questionTitle = question.getTitle(); 70 | String questionBody = question.getBody(); 71 | 72 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { 73 | mTxtQuestionTitle.setText(Html.fromHtml(questionTitle, Html.FROM_HTML_MODE_LEGACY)); 74 | mTxtQuestionBody.setText(Html.fromHtml(questionBody, Html.FROM_HTML_MODE_LEGACY)); 75 | } else { 76 | mTxtQuestionTitle.setText(Html.fromHtml(questionTitle)); 77 | mTxtQuestionBody.setText(Html.fromHtml(questionBody)); 78 | } 79 | } 80 | 81 | 82 | @Override 83 | public void showProgressIndication() { 84 | mProgressBar.setVisibility(View.VISIBLE); 85 | } 86 | 87 | @Override 88 | public void hideProgressIndication() { 89 | mProgressBar.setVisibility(View.GONE); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/questionslist/QuestionsListController.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.questionslist; 2 | 3 | import com.techyourchance.mvc.questions.FetchLastActiveQuestionsUseCase; 4 | import com.techyourchance.mvc.questions.Question; 5 | import com.techyourchance.mvc.screens.common.dialogs.DialogsEventBus; 6 | import com.techyourchance.mvc.screens.common.dialogs.DialogsManager; 7 | import com.techyourchance.mvc.screens.common.dialogs.promptdialog.PromptDialogEvent; 8 | import com.techyourchance.mvc.screens.common.screensnavigator.ScreensNavigator; 9 | 10 | import java.io.Serializable; 11 | import java.util.List; 12 | 13 | public class QuestionsListController implements 14 | QuestionsListViewMvc.Listener, 15 | FetchLastActiveQuestionsUseCase.Listener, 16 | DialogsEventBus.Listener { 17 | 18 | private enum ScreenState { 19 | IDLE, FETCHING_QUESTIONS, QUESTIONS_LIST_SHOWN, NETWORK_ERROR 20 | } 21 | 22 | private final FetchLastActiveQuestionsUseCase mFetchLastActiveQuestionsUseCase; 23 | private final ScreensNavigator mScreensNavigator; 24 | private final DialogsManager mDialogsManager; 25 | private final DialogsEventBus mDialogsEventBus; 26 | 27 | private QuestionsListViewMvc mViewMvc; 28 | 29 | private ScreenState mScreenState = ScreenState.IDLE; 30 | 31 | public QuestionsListController(FetchLastActiveQuestionsUseCase fetchLastActiveQuestionsUseCase, 32 | ScreensNavigator screensNavigator, 33 | DialogsManager dialogsManager, 34 | DialogsEventBus dialogsEventBus) { 35 | mFetchLastActiveQuestionsUseCase = fetchLastActiveQuestionsUseCase; 36 | mScreensNavigator = screensNavigator; 37 | mDialogsManager = dialogsManager; 38 | mDialogsEventBus = dialogsEventBus; 39 | } 40 | 41 | public void bindView(QuestionsListViewMvc viewMvc) { 42 | mViewMvc = viewMvc; 43 | } 44 | 45 | public SavedState getSavedState() { 46 | return new SavedState(mScreenState); 47 | } 48 | 49 | public void restoreSavedState(SavedState savedState) { 50 | mScreenState = savedState.mScreenState; 51 | } 52 | 53 | public void onStart() { 54 | mViewMvc.registerListener(this); 55 | mFetchLastActiveQuestionsUseCase.registerListener(this); 56 | mDialogsEventBus.registerListener(this); 57 | 58 | if (mScreenState != ScreenState.NETWORK_ERROR) { 59 | fetchQuestionsAndNotify(); 60 | } 61 | } 62 | 63 | public void onStop() { 64 | mViewMvc.unregisterListener(this); 65 | mFetchLastActiveQuestionsUseCase.unregisterListener(this); 66 | mDialogsEventBus.unregisterListener(this); 67 | } 68 | 69 | private void fetchQuestionsAndNotify() { 70 | mScreenState = ScreenState.FETCHING_QUESTIONS; 71 | mViewMvc.showProgressIndication(); 72 | mFetchLastActiveQuestionsUseCase.fetchLastActiveQuestionsAndNotify(); 73 | } 74 | 75 | @Override 76 | public void onQuestionClicked(Question question) { 77 | mScreensNavigator.toQuestionDetails(question.getId()); 78 | } 79 | 80 | @Override 81 | public void onLastActiveQuestionsFetched(List questions) { 82 | mScreenState = ScreenState.QUESTIONS_LIST_SHOWN; 83 | mViewMvc.hideProgressIndication(); 84 | mViewMvc.bindQuestions(questions); 85 | } 86 | 87 | @Override 88 | public void onLastActiveQuestionsFetchFailed() { 89 | mScreenState = ScreenState.NETWORK_ERROR; 90 | mViewMvc.hideProgressIndication(); 91 | mDialogsManager.showUseCaseErrorDialog(null); 92 | } 93 | 94 | @Override 95 | public void onDialogEvent(Object event) { 96 | if (event instanceof PromptDialogEvent) { 97 | switch (((PromptDialogEvent) event).getClickedButton()) { 98 | case POSITIVE: 99 | fetchQuestionsAndNotify(); 100 | break; 101 | case NEGATIVE: 102 | mScreenState = ScreenState.IDLE; 103 | break; 104 | } 105 | } 106 | } 107 | 108 | public static class SavedState implements Serializable { 109 | private final ScreenState mScreenState; 110 | 111 | public SavedState(ScreenState screenState) { 112 | mScreenState = screenState; 113 | } 114 | } 115 | 116 | 117 | 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/questionslist/QuestionsListFragment.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.questionslist; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.app.Fragment; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | import com.techyourchance.mvc.screens.common.controllers.BaseFragment; 12 | 13 | public class QuestionsListFragment extends BaseFragment { 14 | 15 | public static Fragment newInstance() { 16 | return new QuestionsListFragment(); 17 | } 18 | 19 | private static final String SAVED_STATE_CONTROLLER = "SAVED_STATE_CONTROLLER"; 20 | 21 | private QuestionsListController mQuestionsListController; 22 | 23 | @Nullable 24 | @Override 25 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 26 | QuestionsListViewMvc viewMvc = getCompositionRoot().getViewMvcFactory().getQuestionsListViewMvc(container); 27 | 28 | mQuestionsListController = getCompositionRoot().getQuestionsListController(); 29 | if (savedInstanceState != null) { 30 | restoreControllerState(savedInstanceState); 31 | } 32 | mQuestionsListController.bindView(viewMvc); 33 | 34 | return viewMvc.getRootView(); 35 | } 36 | 37 | private void restoreControllerState(Bundle savedInstanceState) { 38 | mQuestionsListController.restoreSavedState( 39 | (QuestionsListController.SavedState) 40 | savedInstanceState.getSerializable(SAVED_STATE_CONTROLLER) 41 | ); 42 | } 43 | 44 | @Override 45 | public void onStart() { 46 | super.onStart(); 47 | mQuestionsListController.onStart(); 48 | } 49 | 50 | @Override 51 | public void onStop() { 52 | super.onStop(); 53 | mQuestionsListController.onStop(); 54 | } 55 | 56 | @Override 57 | public void onSaveInstanceState(@NonNull Bundle outState) { 58 | super.onSaveInstanceState(outState); 59 | outState.putSerializable(SAVED_STATE_CONTROLLER, mQuestionsListController.getSavedState()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/questionslist/QuestionsListViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.questionslist; 2 | 3 | import com.techyourchance.mvc.questions.Question; 4 | import com.techyourchance.mvc.screens.common.navdrawer.NavDrawerViewMvc; 5 | import com.techyourchance.mvc.screens.common.views.ObservableViewMvc; 6 | 7 | import java.util.List; 8 | 9 | public interface QuestionsListViewMvc extends ObservableViewMvc { 10 | 11 | public interface Listener { 12 | void onQuestionClicked(Question question); 13 | } 14 | 15 | void bindQuestions(List questions); 16 | 17 | void showProgressIndication(); 18 | 19 | void hideProgressIndication(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/questionslist/QuestionsListViewMvcImpl.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.questionslist; 2 | 3 | import android.support.annotation.Nullable; 4 | import android.support.v7.widget.LinearLayoutManager; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.support.v7.widget.Toolbar; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.ProgressBar; 11 | 12 | import com.techyourchance.mvc.R; 13 | import com.techyourchance.mvc.questions.Question; 14 | import com.techyourchance.mvc.screens.common.navdrawer.NavDrawerHelper; 15 | import com.techyourchance.mvc.screens.common.toolbar.ToolbarViewMvc; 16 | import com.techyourchance.mvc.screens.common.ViewMvcFactory; 17 | import com.techyourchance.mvc.screens.common.views.BaseObservableViewMvc; 18 | 19 | import java.util.List; 20 | 21 | public class QuestionsListViewMvcImpl extends BaseObservableViewMvc 22 | implements QuestionsListViewMvc, QuestionsRecyclerAdapter.Listener { 23 | 24 | private final ToolbarViewMvc mToolbarViewMvc; 25 | private final NavDrawerHelper mNavDrawerHelper; 26 | 27 | private final Toolbar mToolbar; 28 | private final RecyclerView mRecyclerQuestions; 29 | private final QuestionsRecyclerAdapter mAdapter; 30 | private final ProgressBar mProgressBar; 31 | 32 | public QuestionsListViewMvcImpl(LayoutInflater inflater, 33 | @Nullable ViewGroup parent, 34 | NavDrawerHelper navDrawerHelper, 35 | ViewMvcFactory viewMvcFactory) { 36 | mNavDrawerHelper = navDrawerHelper; 37 | setRootView(inflater.inflate(R.layout.layout_questions_list, parent, false)); 38 | 39 | mRecyclerQuestions = findViewById(R.id.recycler_questions); 40 | mRecyclerQuestions.setLayoutManager(new LinearLayoutManager(getContext())); 41 | mAdapter = new QuestionsRecyclerAdapter(this, viewMvcFactory); 42 | mRecyclerQuestions.setAdapter(mAdapter); 43 | 44 | mProgressBar = findViewById(R.id.progress); 45 | 46 | mToolbar = findViewById(R.id.toolbar); 47 | mToolbarViewMvc = viewMvcFactory.getToolbarViewMvc(mToolbar); 48 | initToolbar(); 49 | } 50 | 51 | private void initToolbar() { 52 | mToolbarViewMvc.setTitle(getString(R.string.questions_list_screen_title)); 53 | mToolbar.addView(mToolbarViewMvc.getRootView()); 54 | mToolbarViewMvc.enableHamburgerButtonAndListen(new ToolbarViewMvc.HamburgerClickListener() { 55 | @Override 56 | public void onHamburgerClicked() { 57 | mNavDrawerHelper.openDrawer(); 58 | } 59 | }); 60 | } 61 | 62 | @Override 63 | public void onQuestionClicked(Question question) { 64 | for (Listener listener : getListeners()) { 65 | listener.onQuestionClicked(question); 66 | } 67 | } 68 | 69 | @Override 70 | public void bindQuestions(List questions) { 71 | mAdapter.bindQuestions(questions); 72 | } 73 | 74 | @Override 75 | public void showProgressIndication() { 76 | mProgressBar.setVisibility(View.VISIBLE); 77 | } 78 | 79 | @Override 80 | public void hideProgressIndication() { 81 | mProgressBar.setVisibility(View.GONE); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/questionslist/QuestionsRecyclerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.questionslist; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.ViewGroup; 6 | 7 | import com.techyourchance.mvc.questions.Question; 8 | import com.techyourchance.mvc.screens.common.ViewMvcFactory; 9 | import com.techyourchance.mvc.screens.questionslist.questionslistitem.QuestionsListItemViewMvc; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class QuestionsRecyclerAdapter extends RecyclerView.Adapter 15 | implements QuestionsListItemViewMvc.Listener { 16 | 17 | public interface Listener { 18 | void onQuestionClicked(Question question); 19 | } 20 | 21 | static class MyViewHolder extends RecyclerView.ViewHolder { 22 | 23 | private final QuestionsListItemViewMvc mViewMvc; 24 | 25 | public MyViewHolder(QuestionsListItemViewMvc viewMvc) { 26 | super(viewMvc.getRootView()); 27 | mViewMvc = viewMvc; 28 | } 29 | 30 | } 31 | 32 | private final Listener mListener; 33 | private final ViewMvcFactory mViewMvcFactory; 34 | 35 | private List mQuestions = new ArrayList<>(); 36 | 37 | public QuestionsRecyclerAdapter(Listener listener, ViewMvcFactory viewMvcFactory) { 38 | mListener = listener; 39 | mViewMvcFactory = viewMvcFactory; 40 | } 41 | 42 | public void bindQuestions(List questions) { 43 | mQuestions = new ArrayList<>(questions); 44 | notifyDataSetChanged(); 45 | } 46 | 47 | @NonNull 48 | @Override 49 | public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 50 | QuestionsListItemViewMvc viewMvc = mViewMvcFactory.getQuestionsListItemViewMvc(parent); 51 | viewMvc.registerListener(this); 52 | return new MyViewHolder(viewMvc); 53 | } 54 | 55 | @Override 56 | public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { 57 | holder.mViewMvc.bindQuestion(mQuestions.get(position)); 58 | } 59 | 60 | @Override 61 | public int getItemCount() { 62 | return mQuestions.size(); 63 | } 64 | 65 | @Override 66 | public void onQuestionClicked(Question question) { 67 | mListener.onQuestionClicked(question); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/questionslist/questionslistitem/QuestionsListItemViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.questionslist.questionslistitem; 2 | 3 | import com.techyourchance.mvc.questions.Question; 4 | import com.techyourchance.mvc.screens.common.views.ObservableViewMvc; 5 | 6 | public interface QuestionsListItemViewMvc extends ObservableViewMvc { 7 | 8 | public interface Listener { 9 | void onQuestionClicked(Question question); 10 | } 11 | 12 | void bindQuestion(Question question); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/techyourchance/mvc/screens/questionslist/questionslistitem/QuestionsListItemViewMvcImpl.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mvc.screens.questionslist.questionslistitem; 2 | 3 | import android.support.annotation.Nullable; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.TextView; 8 | 9 | import com.techyourchance.mvc.R; 10 | import com.techyourchance.mvc.questions.Question; 11 | import com.techyourchance.mvc.screens.common.views.BaseObservableViewMvc; 12 | 13 | public class QuestionsListItemViewMvcImpl extends BaseObservableViewMvc 14 | implements QuestionsListItemViewMvc { 15 | 16 | private final TextView mTxtTitle; 17 | 18 | private Question mQuestion; 19 | 20 | public QuestionsListItemViewMvcImpl(LayoutInflater inflater, @Nullable ViewGroup parent) { 21 | setRootView(inflater.inflate(R.layout.layout_question_list_item, parent, false)); 22 | 23 | mTxtTitle = findViewById(R.id.txt_title); 24 | getRootView().setOnClickListener(new View.OnClickListener() { 25 | @Override 26 | public void onClick(View view) { 27 | for (Listener listener : getListeners()) { 28 | listener.onQuestionClicked(mQuestion); 29 | } 30 | } 31 | }); 32 | } 33 | 34 | @Override 35 | public void bindQuestion(Question question) { 36 | mQuestion = question; 37 | mTxtTitle.setText(question.getTitle()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-hdpi/ic_arrow_back.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-hdpi/ic_menu.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_view_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-hdpi/ic_view_list.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-mdpi/ic_arrow_back.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-mdpi/ic_menu.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_view_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-mdpi/ic_view_list.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-xhdpi/ic_arrow_back.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-xhdpi/ic_menu.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_view_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-xhdpi/ic_view_list.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-xxhdpi/ic_arrow_back.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-xxhdpi/ic_menu.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_view_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-xxhdpi/ic_view_list.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-xxxhdpi/ic_arrow_back.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-xxxhdpi/ic_menu.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_view_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/drawable-xxxhdpi/ic_view_list.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_location.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 18 | 19 | 28 | 29 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_prompt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 18 | 19 | 28 | 29 | 38 | 39 | 44 | 45 | 49 | 50 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/layout/element_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_content_frame.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_drawer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_question_details.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 13 | 14 | 20 | 21 | 24 | 25 | 32 | 33 | 40 | 41 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_question_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_questions_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 14 | 15 | 21 | 22 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 22 | 23 | 31 | 32 | 33 | 34 | 35 | 42 | 43 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_drawer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/android-architecture-course-old/7078e97c0f48bb1a2cde59818f14a81d2c01a108/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | #FFFFFF 7 | #000000 8 | #00000000 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MVC 3 | Oops 4 | network call failed 5 | Retry 6 | Close 7 | network call failed 8 | LATEST QUESTIONS 9 | QUESTION DETAILS 10 | Questions list 11 | 12 | Permission dialog 13 | OK 14 | Permission granted 15 | Permission declined and can\'t ask anymore 16 | Permission declined 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |