├── LICENSE ├── README.md └── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── tinmegali │ └── tutsmvp_sample │ └── ApplicationTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── tinmegali │ │ └── tutsmvp_sample │ │ ├── common │ │ └── StateMaintainer.java │ │ ├── data │ │ ├── DAO.java │ │ └── DBSchema.java │ │ ├── main │ │ └── activity │ │ │ ├── MVP_Main.java │ │ │ ├── model │ │ │ └── MainModel.java │ │ │ ├── presenter │ │ │ └── MainPresenter.java │ │ │ └── view │ │ │ ├── MainActivity.java │ │ │ └── recycler │ │ │ └── NotesViewHolder.java │ │ └── models │ │ └── Note.java └── res │ ├── drawable-hdpi │ ├── ic_action_add.png │ └── ic_action_cancel.png │ ├── drawable-ldpi │ ├── ic_action_add.png │ └── ic_action_cancel.png │ ├── drawable-mdpi │ ├── ic_action_add.png │ └── ic_action_cancel.png │ ├── drawable-tvdpi │ ├── ic_action_add.png │ └── ic_action_cancel.png │ ├── drawable-xhdpi │ ├── ic_action_add.png │ └── ic_action_cancel.png │ ├── drawable-xxhdpi │ ├── ic_action_add.png │ └── ic_action_cancel.png │ ├── drawable-xxxhdpi │ ├── ic_action_add.png │ └── ic_action_cancel.png │ ├── layout │ ├── activity_main.xml │ ├── content_main.xml │ └── holder_notes.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-v21 │ └── styles.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── test └── java └── com └── tinmegali └── tutsmvp_sample ├── DBTest.java ├── MainModelTest.java └── MainPresenterTest.java /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Envato Tuts+ 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Envato Tuts+ Tutorial: How to Adopt Model View Presenter on Android 2 | 3 | #### Instructor: Tin Megali 4 | 5 | In the previous tutorial, we talked about the Model View Presenter pattern, how it is applied on Android, and what its most important advantages are. In this tutorial, we explore the Model View Presenter pattern in more detail by implementing it in an Android application. 6 | 7 | Source files for the Envato Tuts+ tutorial: [How to Adopt Model View Presenter on Android](http://code.tutsplus.com/tutorials/how-to-adopt-model-view-presenter-on-android--cms-26206) 8 | 9 | **Read this tutorial on [Envato Tuts+](https://code.tutsplus.com)**. 10 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | applicationId "com.tinmegali.tutsmvp_sample" 9 | minSdkVersion 19 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | testCompile 'junit:junit:4.12' 25 | // Set this dependency if you want to use Hamcrest matching 26 | testCompile 'org.hamcrest:hamcrest-library:1.1' 27 | testCompile "org.robolectric:robolectric:3.0" 28 | testCompile 'org.mockito:mockito-core:1.10.19' 29 | 30 | compile 'com.android.support:appcompat-v7:23.2.0' 31 | compile 'com.android.support:design:23.2.0' 32 | } 33 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/tinmegali/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tinmegali/tutsmvp_sample/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.tinmegali.tutsmvp_sample; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/tinmegali/tutsmvp_sample/common/StateMaintainer.java: -------------------------------------------------------------------------------- 1 | package com.tinmegali.tutsmvp_sample.common; 2 | 3 | /** 4 | * --------------------------------------------------- 5 | * Created by Tin Megali on 19/03/16. 6 | * Project: tuts+mvp_sample 7 | * --------------------------------------------------- 8 | * tinmegali.com 9 | * www.tinmegali.com 29 | * Based on 30 | * framework MVP developed by 31 | * 32 | * Dr. Douglas Schmidth 33 | * 34 | * @see Project's Git
35 | * @see Sample Application 36 | * @see 37 | * Sample MVP interface 38 | * 39 | */ 40 | public class StateMaintainer { 41 | 42 | protected final String TAG = getClass().getSimpleName(); 43 | 44 | private final String mStateMaintenerTag; 45 | private final WeakReference mFragmentManager; 46 | private StateMngFragment mStateMaintainerFrag; 47 | private boolean mIsRecreating; 48 | 49 | /** 50 | * Constructor 51 | */ 52 | public StateMaintainer(FragmentManager fragmentManager, String stateMaintainerTAG) { 53 | mFragmentManager = new WeakReference<>(fragmentManager); 54 | mStateMaintenerTag = stateMaintainerTAG; 55 | } 56 | 57 | /** 58 | * Creates the Fragment responsible to maintain the objects. 59 | * @return true: fragment just created 60 | */ 61 | public boolean firstTimeIn() { 62 | try { 63 | // Recuperando referência 64 | mStateMaintainerFrag = (StateMngFragment) 65 | mFragmentManager.get().findFragmentByTag(mStateMaintenerTag); 66 | 67 | // Criando novo RetainedFragment 68 | if (mStateMaintainerFrag == null) { 69 | Log.d(TAG, "Criando novo RetainedFragment " + mStateMaintenerTag); 70 | mStateMaintainerFrag = new StateMngFragment(); 71 | mFragmentManager.get().beginTransaction() 72 | .add(mStateMaintainerFrag, mStateMaintenerTag).commit(); 73 | mIsRecreating = false; 74 | return true; 75 | } else { 76 | Log.d(TAG, "Retornando retained fragment existente " + mStateMaintenerTag); 77 | mIsRecreating = true; 78 | return false; 79 | } 80 | } catch (NullPointerException e) { 81 | Log.w(TAG, "Erro firstTimeIn()"); 82 | return false; 83 | } 84 | } 85 | 86 | /** 87 | * Return true it the current Activity was recreated at least one time 88 | * @return If the Activity was recreated 89 | */ 90 | public boolean wasRecreated() { return mIsRecreating; } 91 | 92 | 93 | /** 94 | * Insert the object to be preserved. 95 | * @param key object's TAG 96 | * @param obj object to maintain 97 | */ 98 | public void put(String key, Object obj) { 99 | mStateMaintainerFrag.put(key, obj); 100 | } 101 | 102 | /** 103 | * Insert the object to be preserved. 104 | * @param obj object to maintain 105 | */ 106 | public void put(Object obj) { 107 | put(obj.getClass().getName(), obj); 108 | } 109 | 110 | 111 | /** 112 | * Recovers the object saved 113 | * @param key Object's TAG 114 | * @param Object type 115 | * @return Object saved 116 | */ 117 | @SuppressWarnings("unchecked") 118 | public T get(String key) { 119 | return mStateMaintainerFrag.get(key); 120 | 121 | } 122 | 123 | /** 124 | * Checks the existence of a given object 125 | * @param key Key to verification 126 | * @return true: Object exists 127 | */ 128 | public boolean hasKey(String key) { 129 | return mStateMaintainerFrag.get(key) != null; 130 | } 131 | 132 | 133 | /** 134 | * Fragment resposible to preserve objects. 135 | * Instantiated only once. Uses a hashmap to save objs 136 | */ 137 | public static class StateMngFragment extends Fragment { 138 | private HashMap mData = new HashMap<>(); 139 | 140 | @Override 141 | public void onCreate(Bundle savedInstanceState) { 142 | super.onCreate(savedInstanceState); 143 | // Grants that the fragment will be preserved 144 | setRetainInstance(true); 145 | } 146 | 147 | /** 148 | * Insert objects on the hashmap 149 | * @param key Reference key 150 | * @param obj Object to be saved 151 | */ 152 | public void put(String key, Object obj) { 153 | mData.put(key, obj); 154 | } 155 | 156 | /** 157 | * Insert objects on the hashmap 158 | * @param object Object to be saved 159 | */ 160 | public void put(Object object) { 161 | put(object.getClass().getName(), object); 162 | } 163 | 164 | /** 165 | * Recovers saved object 166 | * @param key Reference key 167 | * @param Object type 168 | * @return Object saved 169 | */ 170 | @SuppressWarnings("unchecked") 171 | public T get(String key) { 172 | return (T) mData.get(key); 173 | } 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /app/src/main/java/com/tinmegali/tutsmvp_sample/data/DAO.java: -------------------------------------------------------------------------------- 1 | package com.tinmegali.tutsmvp_sample.data; 2 | 3 | import android.content.Context; 4 | import android.database.Cursor; 5 | import android.database.sqlite.SQLiteDatabase; 6 | 7 | import com.tinmegali.tutsmvp_sample.models.Note; 8 | 9 | import java.util.ArrayList; 10 | 11 | /** 12 | * --------------------------------------------------- 13 | * Created by Tin Megali on 18/03/16. 14 | * Project: tuts+mvp_sample 15 | * --------------------------------------------------- 16 | * tinmegali.com 17 | * getAllNotes() { 68 | SQLiteDatabase db = getReadDB(); 69 | Cursor c = db.query( 70 | DBSchema.TABLE_NOTES, 71 | null, 72 | null, 73 | null, null, null, 74 | SORT_ORDER_DEFAULT 75 | ); 76 | if ( c!= null) { 77 | c.moveToFirst(); 78 | ArrayList notes = new ArrayList<>(); 79 | while (!c.isAfterLast()) { 80 | Note note = new Note(); 81 | note.setId( c.getInt( c.getColumnIndexOrThrow( DBSchema.TB_NOTES.ID ))); 82 | note.setText(c.getString(c.getColumnIndexOrThrow(DBSchema.TB_NOTES.NOTE))); 83 | note.setDate(c.getString(c.getColumnIndexOrThrow(DBSchema.TB_NOTES.DATE))); 84 | notes.add(note); 85 | c.moveToNext(); 86 | } 87 | c.close(); 88 | db.close(); 89 | return notes; 90 | } else { 91 | return null; 92 | } 93 | } 94 | 95 | public Note getNote(int id){ 96 | SQLiteDatabase db = getReadDB(); 97 | Cursor c = db.query( 98 | DBSchema.TABLE_NOTES, 99 | null, 100 | SELECT_ID_BASED, 101 | new String[]{Integer.toString(id)}, 102 | null, 103 | null, 104 | null 105 | ); 106 | if (c != null) { 107 | c.moveToFirst(); 108 | Note note = new Note(); 109 | note.setId(c.getInt(c.getColumnIndexOrThrow(DBSchema.TB_NOTES.ID))); 110 | note.setText(c.getString(c.getColumnIndexOrThrow(DBSchema.TB_NOTES.NOTE))); 111 | note.setDate(c.getString(c.getColumnIndexOrThrow(DBSchema.TB_NOTES.DATE))); 112 | c.close(); 113 | db.close(); 114 | return note; 115 | } else return null; 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/tinmegali/tutsmvp_sample/data/DBSchema.java: -------------------------------------------------------------------------------- 1 | package com.tinmegali.tutsmvp_sample.data; 2 | 3 | import android.content.Context; 4 | import android.database.sqlite.SQLiteDatabase; 5 | import android.database.sqlite.SQLiteOpenHelper; 6 | 7 | /** 8 | * --------------------------------------------------- 9 | * Created by Tin Megali on 18/03/16. 10 | * Project: tuts+mvp_sample 11 | * --------------------------------------------------- 12 | * tinmegali.com 13 | * tinmegali.com 28 | * tinmegali.com 17 | * tinmegali.com 32 | * (view); 91 | } 92 | 93 | /** 94 | * Called by Activity during MVP setup. Only called once. 95 | * @param model Model instance 96 | */ 97 | public void setModel(MVP_Main.ProvidedModelOps model) { 98 | mModel = model; 99 | 100 | // start to load data 101 | loadData(); 102 | } 103 | 104 | /** 105 | * Load data from Model in a AsyncTask 106 | */ 107 | private void loadData() { 108 | try { 109 | getView().showProgress(); 110 | new AsyncTask() { 111 | @Override 112 | protected Boolean doInBackground(Void... params) { 113 | // Load data from Model 114 | return mModel.loadData(); 115 | } 116 | 117 | @Override 118 | protected void onPostExecute(Boolean result) { 119 | try { 120 | getView().hideProgress(); 121 | if (!result) // Loading error 122 | getView().showToast(makeToast("Error loading data.")); 123 | else // success 124 | getView().notifyDataSetChanged(); 125 | } catch (NullPointerException e) { 126 | e.printStackTrace(); 127 | } 128 | } 129 | }.execute(); 130 | } catch (NullPointerException e) { 131 | e.printStackTrace(); 132 | } 133 | } 134 | 135 | /** 136 | * Creat a Toast object with given message 137 | * @param msg Toast message 138 | * @return A Toast object 139 | */ 140 | private Toast makeToast(String msg) { 141 | return Toast.makeText(getView().getAppContext(), msg, Toast.LENGTH_SHORT); 142 | } 143 | 144 | /** 145 | * Retrieve total Notes count from Model 146 | * @return Notes size 147 | */ 148 | @Override 149 | public int getNotesCount() { 150 | return mModel.getNotesCount(); 151 | } 152 | 153 | /** 154 | * Create the RecyclerView holder and setup its view 155 | * @param parent Recycler viewgroup 156 | * @param viewType Holder type 157 | * @return Recycler ViewHolder 158 | */ 159 | @Override 160 | public NotesViewHolder createViewHolder(ViewGroup parent, int viewType) { 161 | NotesViewHolder viewHolder; 162 | LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 163 | 164 | View viewTaskRow = inflater.inflate(R.layout.holder_notes, parent, false); 165 | viewHolder = new NotesViewHolder(viewTaskRow); 166 | 167 | return viewHolder; 168 | } 169 | 170 | /** 171 | * Binds ViewHolder with RecyclerView 172 | * @param holder Holder to bind 173 | * @param position Position on Recycler adapter 174 | */ 175 | @Override 176 | public void bindViewHolder(final NotesViewHolder holder, int position) { 177 | final Note note = mModel.getNote(position); 178 | holder.text.setText( note.getText() ); 179 | holder.date.setText( note.getDate() ); 180 | holder.btnDelete.setOnClickListener(new View.OnClickListener() { 181 | @Override 182 | public void onClick(View v) { 183 | clickDeleteNote(note, holder.getAdapterPosition(), holder.getLayoutPosition()); 184 | } 185 | }); 186 | 187 | } 188 | 189 | /** 190 | * Retrieve Application Context 191 | * @return Application context 192 | */ 193 | @Override 194 | public Context getAppContext() { 195 | try { 196 | return getView().getAppContext(); 197 | } catch (NullPointerException e) { 198 | return null; 199 | } 200 | } 201 | 202 | /** 203 | * Retrieves Activity context 204 | * @return Activity context 205 | */ 206 | @Override 207 | public Context getActivityContext() { 208 | try { 209 | return getView().getActivityContext(); 210 | } catch (NullPointerException e) { 211 | return null; 212 | } 213 | } 214 | 215 | /** 216 | * Called by View when user clicks on new Note btn. 217 | * Creates a Note with text typed by the user and asks 218 | * Model to insert in DB. 219 | * @param editText EdiText with text typed by user 220 | */ 221 | @Override 222 | public void clickNewNote(final EditText editText) { 223 | getView().showProgress(); 224 | final String noteText = editText.getText().toString(); 225 | if ( !noteText.isEmpty() ) { 226 | new AsyncTask() { 227 | @Override 228 | protected Integer doInBackground(Void... params) { 229 | // Insert note in Model, returning result 230 | return mModel.insertNote(makeNote(noteText)); 231 | } 232 | 233 | @Override 234 | protected void onPostExecute(Integer adapterPosition) { 235 | try { 236 | if (adapterPosition > -1) { 237 | // Insert note 238 | getView().clearEditText(); 239 | getView().notifyItemInserted(adapterPosition + 1); 240 | getView().notifyItemRangeChanged(adapterPosition, mModel.getNotesCount()); 241 | } else { 242 | // Inform about error 243 | getView().hideProgress(); 244 | getView().showToast(makeToast("Error creating note [" + noteText + "]")); 245 | } 246 | } catch (NullPointerException e) { 247 | e.printStackTrace(); 248 | } 249 | } 250 | }.execute(); 251 | } else { 252 | try { 253 | getView().showToast(makeToast("Cannot add a blank note!")); 254 | } catch (NullPointerException e) { 255 | e.printStackTrace(); 256 | } 257 | } 258 | } 259 | 260 | /** 261 | * Create a Note object with giver text 262 | * @param noteText String with Note text 263 | * @return A Note object 264 | */ 265 | public Note makeNote(String noteText) { 266 | Note note = new Note(); 267 | note.setText( noteText ); 268 | note.setDate(getDate()); 269 | return note; 270 | 271 | } 272 | 273 | /** 274 | * Get current Date as a String 275 | * @return The current date 276 | */ 277 | private String getDate() { 278 | return new SimpleDateFormat("HH:mm:ss - MM/dd/yyyy", Locale.getDefault()).format(new Date()); 279 | } 280 | 281 | /** 282 | * Called by View when user click on delete button. 283 | * Create a AlertBox to confirm the action. 284 | * @param note Note to delete 285 | * @param adapterPos Position on adapter 286 | * @param layoutPos Layout position on RecyclerView 287 | */ 288 | @Override 289 | public void clickDeleteNote(final Note note, final int adapterPos, final int layoutPos) { 290 | openDeleteAlert(note, adapterPos, layoutPos); 291 | } 292 | 293 | /** 294 | * Create an AlertBox to confirm a delete action 295 | * @param note Note to be deleted 296 | * @param adapterPos Adapter postion 297 | * @param layoutPos Recycler layout position 298 | */ 299 | private void openDeleteAlert(final Note note, final int adapterPos, final int layoutPos){ 300 | AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivityContext()); 301 | alertBuilder.setPositiveButton("DELETE", new DialogInterface.OnClickListener() { 302 | @Override 303 | public void onClick(DialogInterface dialog, int which) { 304 | // Delete note if action is confirmed 305 | deleteNote(note, adapterPos, layoutPos); 306 | } 307 | }); 308 | alertBuilder.setNegativeButton("CANCEL", new DialogInterface.OnClickListener() { 309 | @Override 310 | public void onClick(DialogInterface dialog, int which) { 311 | dialog.dismiss(); 312 | } 313 | }); 314 | alertBuilder.setTitle("Delete Note"); 315 | alertBuilder.setMessage("Delete " + note.getText() + " ?"); 316 | 317 | AlertDialog alertDialog = alertBuilder.create(); 318 | try { 319 | getView().showAlert(alertDialog); 320 | } catch (NullPointerException e) { 321 | e.printStackTrace(); 322 | } 323 | } 324 | 325 | /** 326 | * Create a asyncTask to delete the object in Model 327 | * @param note Note to delete 328 | * @param adapterPos Adapter position 329 | * @param layoutPos Recycler layout position 330 | */ 331 | public void deleteNote(final Note note, final int adapterPos, final int layoutPos) { 332 | getView().showProgress(); 333 | new AsyncTask() { 334 | @Override 335 | protected Boolean doInBackground(Void... params) { 336 | // Delete note on Model, returning result 337 | return mModel.deleteNote(note, adapterPos); 338 | } 339 | 340 | @Override 341 | protected void onPostExecute(Boolean result) { 342 | try { 343 | getView().hideProgress(); 344 | if ( result ) { 345 | // Remove item from RecyclerView 346 | getView().notifyItemRemoved(layoutPos); 347 | getView().showToast(makeToast("Note deleted.")); 348 | } else { 349 | // Inform about error 350 | getView().showToast(makeToast("Error deleting note["+note.getId()+"]")); 351 | } 352 | } catch (NullPointerException e) { 353 | e.printStackTrace(); 354 | } 355 | 356 | } 357 | }.execute(); 358 | } 359 | 360 | 361 | } 362 | -------------------------------------------------------------------------------- /app/src/main/java/com/tinmegali/tutsmvp_sample/main/activity/view/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.tinmegali.tutsmvp_sample.main.activity.view; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.support.design.widget.FloatingActionButton; 6 | import android.support.v7.app.AlertDialog; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.support.v7.widget.DefaultItemAnimator; 9 | import android.support.v7.widget.LinearLayoutManager; 10 | import android.support.v7.widget.RecyclerView; 11 | import android.support.v7.widget.Toolbar; 12 | import android.view.View; 13 | import android.view.Menu; 14 | import android.view.MenuItem; 15 | import android.view.ViewGroup; 16 | import android.widget.EditText; 17 | import android.widget.ProgressBar; 18 | import android.widget.Toast; 19 | 20 | import com.tinmegali.tutsmvp_sample.R; 21 | import com.tinmegali.tutsmvp_sample.common.StateMaintainer; 22 | import com.tinmegali.tutsmvp_sample.main.activity.MVP_Main; 23 | import com.tinmegali.tutsmvp_sample.main.activity.model.MainModel; 24 | import com.tinmegali.tutsmvp_sample.main.activity.presenter.MainPresenter; 25 | import com.tinmegali.tutsmvp_sample.main.activity.view.recycler.NotesViewHolder; 26 | 27 | public class MainActivity 28 | extends AppCompatActivity 29 | implements View.OnClickListener, MVP_Main.RequiredViewOps 30 | { 31 | 32 | private EditText mTextNewNote; 33 | private ListNotes mListAdapter; 34 | private ProgressBar mProgress; 35 | 36 | private MVP_Main.ProvidedPresenterOps mPresenter; 37 | 38 | // Responsible to maintain the object's integrity 39 | // during configurations change 40 | private final StateMaintainer mStateMaintainer = 41 | new StateMaintainer( getFragmentManager(), MainActivity.class.getName()); 42 | 43 | @Override 44 | protected void onCreate(Bundle savedInstanceState) { 45 | super.onCreate(savedInstanceState); 46 | setContentView(R.layout.activity_main); 47 | 48 | setupViews(); 49 | setupMVP(); 50 | } 51 | 52 | @Override 53 | protected void onDestroy() { 54 | super.onDestroy(); 55 | mPresenter.onDestroy(isChangingConfigurations()); 56 | } 57 | 58 | /** 59 | * Setup the Views 60 | */ 61 | private void setupViews(){ 62 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 63 | setSupportActionBar(toolbar); 64 | FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); 65 | fab.setOnClickListener(this); 66 | 67 | mTextNewNote = (EditText) findViewById(R.id.edit_note); 68 | mListAdapter = new ListNotes(); 69 | mProgress = (ProgressBar) findViewById(R.id.progressbar); 70 | 71 | RecyclerView mList = (RecyclerView) findViewById(R.id.list_notes); 72 | LinearLayoutManager linearLayoutManager = new LinearLayoutManager( this ); 73 | linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); 74 | 75 | mList.setLayoutManager(linearLayoutManager); 76 | mList.setAdapter(mListAdapter); 77 | mList.setItemAnimator(new DefaultItemAnimator()); 78 | } 79 | 80 | /** 81 | * Setup Model View Presenter pattern. 82 | * Use a {@link StateMaintainer} to maintain the 83 | * Presenter and Model instances between configuration changes. 84 | * Could be done differently, 85 | * using a dependency injection for example. 86 | */ 87 | private void setupMVP() { 88 | // Check if StateMaintainer has been created 89 | if (mStateMaintainer.firstTimeIn()) { 90 | // Create the Presenter 91 | MainPresenter presenter = new MainPresenter(this); 92 | // Create the Model 93 | MainModel model = new MainModel(presenter); 94 | // Set Presenter model 95 | presenter.setModel(model); 96 | // Add Presenter and Model to StateMaintainer 97 | mStateMaintainer.put(presenter); 98 | mStateMaintainer.put(model); 99 | 100 | // Set the Presenter as a interface 101 | // To limit the communication with it 102 | mPresenter = presenter; 103 | 104 | } 105 | // get the Presenter from StateMaintainer 106 | else { 107 | // Get the Presenter 108 | mPresenter = mStateMaintainer.get(MainPresenter.class.getName()); 109 | // Updated the View in Presenter 110 | mPresenter.setView(this); 111 | } 112 | } 113 | 114 | 115 | @Override 116 | public void onClick(View v) { 117 | switch (v.getId()) { 118 | case R.id.fab:{ 119 | // Add new note 120 | mPresenter.clickNewNote(mTextNewNote); 121 | } 122 | } 123 | } 124 | 125 | @Override 126 | public Context getActivityContext() { 127 | return this; 128 | } 129 | 130 | @Override 131 | public Context getAppContext() { 132 | return getApplicationContext(); 133 | } 134 | 135 | @Override 136 | public void showToast(Toast toast) { 137 | toast.show(); 138 | } 139 | 140 | @Override 141 | public void clearEditText() { 142 | mTextNewNote.setText(""); 143 | } 144 | 145 | @Override 146 | public void showProgress() { 147 | mProgress.setVisibility(View.VISIBLE);; 148 | } 149 | 150 | @Override 151 | public void hideProgress() { 152 | mProgress.setVisibility(View.GONE);; 153 | } 154 | 155 | @Override 156 | public void showAlert(AlertDialog dialog) { 157 | dialog.show(); 158 | } 159 | 160 | @Override 161 | public void notifyItemRemoved(int position) { 162 | mListAdapter.notifyItemRemoved(position); 163 | } 164 | 165 | @Override 166 | public void notifyItemInserted(int adapterPos) { 167 | mListAdapter.notifyItemInserted(adapterPos); 168 | } 169 | 170 | @Override 171 | public void notifyItemRangeChanged(int positionStart, int itemCount){ 172 | mListAdapter.notifyItemRangeChanged(positionStart, itemCount); 173 | } 174 | 175 | @Override 176 | public void notifyDataSetChanged() { 177 | mListAdapter.notifyDataSetChanged(); 178 | } 179 | 180 | private class ListNotes extends RecyclerView.Adapter { 181 | 182 | 183 | @Override 184 | public int getItemCount() { 185 | return mPresenter.getNotesCount(); 186 | } 187 | 188 | @Override 189 | public NotesViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 190 | return mPresenter.createViewHolder(parent, viewType); 191 | } 192 | 193 | @Override 194 | public void onBindViewHolder(NotesViewHolder holder, int position) { 195 | mPresenter.bindViewHolder(holder, position); 196 | } 197 | 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /app/src/main/java/com/tinmegali/tutsmvp_sample/main/activity/view/recycler/NotesViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.tinmegali.tutsmvp_sample.main.activity.view.recycler; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.View; 5 | import android.widget.ImageButton; 6 | import android.widget.LinearLayout; 7 | import android.widget.RelativeLayout; 8 | import android.widget.TextView; 9 | 10 | import com.tinmegali.tutsmvp_sample.R; 11 | 12 | /** 13 | * --------------------------------------------------- 14 | * Created by Tin Megali on 18/03/16. 15 | * Project: tuts+mvp_sample 16 | * --------------------------------------------------- 17 | * tinmegali.com 18 | * tinmegali.com 13 | * 2 | 9 | 10 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 20 | 21 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/holder_notes.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 23 | 24 | 29 | 30 | 31 | 32 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tutsplus/Android-ModelViewPresenter-Example/74dbc35cef799610cf64153247a75d6ad3a2d630/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tutsplus/Android-ModelViewPresenter-Example/74dbc35cef799610cf64153247a75d6ad3a2d630/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tutsplus/Android-ModelViewPresenter-Example/74dbc35cef799610cf64153247a75d6ad3a2d630/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tutsplus/Android-ModelViewPresenter-Example/74dbc35cef799610cf64153247a75d6ad3a2d630/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tutsplus/Android-ModelViewPresenter-Example/74dbc35cef799610cf64153247a75d6ad3a2d630/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | > 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #318bff 4 | #30649f 5 | #d72629 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | tuts+mvp_sample 3 | Settings 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/test/java/com/tinmegali/tutsmvp_sample/DBTest.java: -------------------------------------------------------------------------------- 1 | package com.tinmegali.tutsmvp_sample; 2 | 3 | import android.content.Context; 4 | 5 | import com.tinmegali.tutsmvp_sample.data.DAO; 6 | import com.tinmegali.tutsmvp_sample.models.Note; 7 | 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.robolectric.RobolectricGradleTestRunner; 12 | import org.robolectric.RuntimeEnvironment; 13 | import org.robolectric.annotation.Config; 14 | 15 | import java.util.ArrayList; 16 | 17 | import static org.junit.Assert.*; 18 | 19 | /** 20 | * --------------------------------------------------- 21 | * Created by Tin Megali on 18/03/16. 22 | * Project: tuts+mvp_sample 23 | * --------------------------------------------------- 24 | * tinmegali.com 25 | * noteTexts = new ArrayList<>(); 61 | noteTexts.add( "note1" ); 62 | noteTexts.add( "note2" ); 63 | noteTexts.add("note3"); 64 | 65 | for( int i=0; i notes = dao.getAllNotes(); 71 | assertNotNull( notes ); 72 | assertEquals(notes.size(), noteTexts.size()); 73 | } 74 | 75 | @Test 76 | public void deleteNote() { 77 | Note note = getNote("deleteNote"); 78 | Note noteInserted = dao.insertNote(note); 79 | assertNotNull(noteInserted); 80 | 81 | Note noteFetched = dao.getNote(noteInserted.getId()); 82 | assertNotNull(noteFetched); 83 | assertEquals(noteFetched.getText(), note.getText()); 84 | 85 | long delResult = dao.deleteNote( noteInserted ); 86 | assertEquals(1, delResult); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/test/java/com/tinmegali/tutsmvp_sample/MainModelTest.java: -------------------------------------------------------------------------------- 1 | package com.tinmegali.tutsmvp_sample; 2 | 3 | import android.content.Context; 4 | 5 | import com.tinmegali.tutsmvp_sample.data.DAO; 6 | import com.tinmegali.tutsmvp_sample.main.activity.model.MainModel; 7 | import com.tinmegali.tutsmvp_sample.main.activity.presenter.MainPresenter; 8 | import com.tinmegali.tutsmvp_sample.models.Note; 9 | 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mockito; 14 | import org.robolectric.RobolectricGradleTestRunner; 15 | import org.robolectric.RuntimeEnvironment; 16 | import org.robolectric.annotation.Config; 17 | 18 | import java.util.ArrayList; 19 | 20 | import static org.mockito.Mockito.*; 21 | import static org.junit.Assert.*; 22 | 23 | /** 24 | * --------------------------------------------------- 25 | * Created by Tin Megali on 18/03/16. 26 | * Project: tuts+mvp_sample 27 | * --------------------------------------------------- 28 | * tinmegali.com 29 | * (); 47 | 48 | reset(mockPresenter); 49 | } 50 | 51 | 52 | private Note createNote(String text) { 53 | Note note = new Note(); 54 | note.setText(text); 55 | note.setDate("some date"); 56 | return note; 57 | } 58 | 59 | @Test 60 | public void loadData(){ 61 | 62 | int notesSize = 10; 63 | for (int i =0; i -1); 76 | } 77 | 78 | @Test 79 | public void deleteNote() { 80 | Note note = createNote("testNote"); 81 | Note insertedNote = mDAO.insertNote(note); 82 | mModel.mNotes = new ArrayList<>(); 83 | mModel.mNotes.add(insertedNote); 84 | 85 | assertTrue(mModel.deleteNote(insertedNote, 0)); 86 | 87 | Note fakeNote = createNote("fakeNote"); 88 | assertFalse(mModel.deleteNote(fakeNote, 0)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/test/java/com/tinmegali/tutsmvp_sample/MainPresenterTest.java: -------------------------------------------------------------------------------- 1 | package com.tinmegali.tutsmvp_sample; 2 | 3 | import android.widget.EditText; 4 | import android.widget.Toast; 5 | 6 | import com.tinmegali.tutsmvp_sample.main.activity.MVP_Main; 7 | import com.tinmegali.tutsmvp_sample.main.activity.model.MainModel; 8 | import com.tinmegali.tutsmvp_sample.main.activity.presenter.MainPresenter; 9 | import com.tinmegali.tutsmvp_sample.models.Note; 10 | 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.Mockito; 15 | import org.robolectric.RobolectricGradleTestRunner; 16 | import org.robolectric.annotation.Config; 17 | 18 | import static org.mockito.Mockito.*; 19 | 20 | /** 21 | * --------------------------------------------------- 22 | * Created by Tin Megali on 18/03/16. 23 | * Project: tuts+mvp_sample 24 | * --------------------------------------------------- 25 | * tinmegali.com 26 | *