getListOfData();
34 |
35 | ListItem createNewListItem();
36 |
37 | void deleteListItem(ListItem listItem);
38 |
39 | void insertListItem(ListItem temporaryListItem);
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/forgettery/wiseass/com/recyclerviewprebuilt/data/FakeDataSource.java:
--------------------------------------------------------------------------------
1 | /*
2 | * *
3 | * * Copyright (C) 2017 Ryan Kay Open Source Project
4 | * *
5 | * * Licensed under the Apache License, Version 2.0 (the "License");
6 | * * you may not use this file except in compliance with the License.
7 | * * You may obtain a copy of the License at
8 | * *
9 | * * http://www.apache.org/licenses/LICENSE-2.0
10 | * *
11 | * * Unless required by applicable law or agreed to in writing, software
12 | * * distributed under the License is distributed on an "AS IS" BASIS,
13 | * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * * See the License for the specific language governing permissions and
15 | * * limitations under the License.
16 | *
17 | */
18 |
19 | package forgettery.wiseass.com.recyclerviewprebuilt.data;
20 |
21 | import java.util.ArrayList;
22 | import java.util.List;
23 | import java.util.Random;
24 |
25 | import forgettery.wiseass.com.recyclerviewprebuilt.R;
26 |
27 | /**
28 | * "Fake objects actually have working implementations, but usually take some shortcut which makes
29 | * them not suitable for production (an in memory database is a good example)."
30 | *
31 | * -Martin Fowler,
32 | * https://www.martinfowler.com/articles/mocksArentStubs.html
33 | *
34 | * Since this is a tutorial about building a RecyclerView, our Activity will be talking to a "Fake"
35 | * DataSource. As long as our "Fake" Data looks the same as the Data we'd expect from a "Real"
36 | * Datasource, we can use these Fakes to Test our App!
37 | *
38 | *
39 | * Created by R_KAY on 6/1/2017.
40 | */
41 |
42 | public class FakeDataSource implements DataSourceInterface{
43 |
44 | private static final int sizeOfCollection = 12;
45 | private Random random;
46 |
47 | private final String[] datesAndTimes = {
48 | "6:30AM 06/01/2017",
49 | "9:26PM 04/22/2013",
50 | "2:01PM 12/02/2015",
51 | "2:43AM 09/7/2018",
52 | };
53 |
54 | private final String[] messages = {
55 | "Check out content like Fragmented Podcast to expose yourself to the knowledge, ideas, " +
56 | "and opinions of experts in your field",
57 | "Look at Open Source Projects like Android Architecture Blueprints to see how experts" +
58 | " design and build Apps",
59 | "Write lots of Code and Example Apps. Writing good Quality Code in an efficient manner "
60 | + "is a Skill to be practiced like any other.",
61 | "If at first something doesn't make any sense, find another explanation. We all " +
62 | "learn/teach different from each other. Find an explanation that speaks to you."
63 | };
64 |
65 | private final int[] drawables = {
66 | R.drawable.green_drawable,
67 | R.drawable.red_drawable,
68 | R.drawable.blue_drawable,
69 | R.drawable.yellow_drawable
70 | };
71 |
72 |
73 | public FakeDataSource() {
74 | random = new Random();
75 | }
76 |
77 | /**
78 | * Creates a list of ListItems.
79 | *
80 | * @return A list of 12 semi-random ListItems for testing purposes
81 | */
82 | @Override
83 | public List getListOfData() {
84 | ArrayList listOfData = new ArrayList<>();
85 | Random random = new Random();
86 | //make 12 semi-random items
87 | for (int i = 0; i < 12; i++) {
88 |
89 | listOfData.add(
90 | createNewListItem()
91 | );
92 | }
93 |
94 | return listOfData;
95 | }
96 |
97 | @Override
98 | public ListItem createNewListItem() {
99 |
100 | //these will be 0, 1, 2, or 3
101 | int randOne = random.nextInt(4);
102 | int randTwo = random.nextInt(4);
103 | int randThree = random.nextInt(4);
104 |
105 | //creates a semi-random ListItem
106 | ListItem listItem = new ListItem(
107 | datesAndTimes[randOne],
108 | messages[randTwo],
109 | drawables[randThree]
110 | );
111 |
112 | return listItem;
113 | }
114 |
115 | @Override
116 | public void deleteListItem(ListItem listItem) {
117 |
118 | }
119 |
120 | @Override
121 | public void insertListItem(ListItem temporaryListItem) {
122 |
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/app/src/main/java/forgettery/wiseass/com/recyclerviewprebuilt/data/ListItem.java:
--------------------------------------------------------------------------------
1 | /*
2 | * *
3 | * * Copyright (C) 2017 Ryan Kay Open Source Project
4 | * *
5 | * * Licensed under the Apache License, Version 2.0 (the "License");
6 | * * you may not use this file except in compliance with the License.
7 | * * You may obtain a copy of the License at
8 | * *
9 | * * http://www.apache.org/licenses/LICENSE-2.0
10 | * *
11 | * * Unless required by applicable law or agreed to in writing, software
12 | * * distributed under the License is distributed on an "AS IS" BASIS,
13 | * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * * See the License for the specific language governing permissions and
15 | * * limitations under the License.
16 | *
17 | */
18 |
19 | package forgettery.wiseass.com.recyclerviewprebuilt.data;
20 |
21 | /**
22 | * 3.
23 | * This class describes an "Item" of "Data". In a real App, I'd call them things like:
24 | * - Workout
25 | * - Reminder
26 | * - Task
27 | * - Note
28 | * - User
29 | *
30 | * Created by R_KAY on 5/31/2017.
31 | */
32 |
33 | public class ListItem {
34 |
35 | private String dateAndTime;
36 | private String message;
37 | private int colorResource;
38 |
39 | /*It's common for an "Item" to have a unique Id for storing an a Database
40 | private String uniqueIdentifier;*/
41 |
42 | public ListItem(String dateAndTime, String message, int colorResource) {
43 | this.dateAndTime = dateAndTime;
44 | this.message = message;
45 | this.colorResource = colorResource;
46 | }
47 |
48 | public int getColorResource() {
49 | return colorResource;
50 | }
51 |
52 | public void setColorResource(int colorResource) {
53 | this.colorResource = colorResource;
54 | }
55 |
56 | public String getDateAndTime() {
57 | return dateAndTime;
58 | }
59 |
60 | public void setDateAndTime(String dateAndTime) {
61 | this.dateAndTime = dateAndTime;
62 | }
63 |
64 |
65 | public String getMessage() {
66 | return message;
67 | }
68 |
69 | public void setMessage(String message) {
70 | this.message = message;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/java/forgettery/wiseass/com/recyclerviewprebuilt/logic/Controller.java:
--------------------------------------------------------------------------------
1 | /*
2 | * *
3 | * * Copyright (C) 2017 Ryan Kay Open Source Project
4 | * *
5 | * * Licensed under the Apache License, Version 2.0 (the "License");
6 | * * you may not use this file except in compliance with the License.
7 | * * You may obtain a copy of the License at
8 | * *
9 | * * http://www.apache.org/licenses/LICENSE-2.0
10 | * *
11 | * * Unless required by applicable law or agreed to in writing, software
12 | * * distributed under the License is distributed on an "AS IS" BASIS,
13 | * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * * See the License for the specific language governing permissions and
15 | * * limitations under the License.
16 | *
17 | */
18 |
19 | package forgettery.wiseass.com.recyclerviewprebuilt.logic;
20 |
21 | import android.view.View;
22 |
23 | import forgettery.wiseass.com.recyclerviewprebuilt.data.DataSourceInterface;
24 | import forgettery.wiseass.com.recyclerviewprebuilt.data.ListItem;
25 | import forgettery.wiseass.com.recyclerviewprebuilt.view.ViewInterface;
26 |
27 | /**
28 | * 16.
29 | * It's an unfortunate fact that in Programming, people like to use many names for the same thing,
30 | * and one name for many things at the same time. I would normally call a class like this a
31 | * "Presenter", since I currently use Model-View-Presenter (Passive View) style Architecture in my
32 | * own Apps. However, since this isn't an MVP App but a RecyclerView tutorial which is likely to be
33 | * watched by a fair number of Beginners, let me explain this a little further:
34 | *
35 | * Whether it's MVP, MVC, MVVM, etc., a rough (and INCOMPLETE!!!) idea of how these three layer
36 | * Architectures seperate their concerns (what they do) is:
37 | *
38 | * 1. Drawing things on the Screen and Listening for Click Events Layer. In Android, this will
39 | * almost always be a Fragment or Activity (although there are other options to create Views with
40 | * 3rd Party Libs). Most Architectures will call this the "View". In most cases, it doesn't
41 | * make it's own decisions, but simply passes "Events" to Layer 2, and let's Layer 2 do the
42 | * thinking.
43 | *
44 | * 2. Decision Making and Communication Layer. This Layer will generally have the responsibility of
45 | * coordinating "Events" from other Layers, such as a "Click" on the UI, or an Error Message from
46 | * the Database. It receives these events, and then using Logic, decides how the App will respond to
47 | * such events. It turns out that if you write your App well, there will be a relatively finite
48 | * number of "Events" and "Decisions" we can choose based on those events. Controller and Presenter
49 | * are common names for Classes in this Layer, but at this point there seems very little consistency
50 | * for naming this Layer.
51 | *
52 | *
53 | * 3. Data Layer. This Layer manages access to a Database/Repository of some kind. It could be Local
54 | * (stored on the device) or it could be remote (cloud storage). Generally it will just manage
55 | * whatever Database it is using, and provide Layer 2 with Methods it can use to Create, Read,
56 | * Update, Delete (CRUD) Data, and error Messages from the Database.
57 | *
58 | * Created by R_KAY on 6/3/2017.
59 | */
60 |
61 | public class Controller {
62 |
63 | private ListItem temporaryListItem;
64 | private int temporaryListItemPosition;
65 |
66 | /*
67 | All that's going on with these Variables, is that we're talking to both ListActivity and
68 | FakeDataSource through Interfaces. This has many benefits, but I'd invite you to research
69 | "Code to an Interface" for a fairly clear example.
70 | */
71 | private ViewInterface view;
72 |
73 |
74 | private DataSourceInterface dataSource;
75 |
76 | /**
77 | * As soon as this object is created, it does a few things:
78 | * 1. Assigns Interfaces Variables so that it can talk to the DataSource and that Activity
79 | * 2. Tells the dataSource to fetch a List of ListItems.
80 | * 3. Tells the View to draw the fetched List of Data.
81 | * @param view
82 | * @param dataSource
83 | */
84 | public Controller(ViewInterface view, DataSourceInterface dataSource) {
85 | this.view = view;
86 | this.dataSource = dataSource;
87 |
88 | getListFromDataSource();
89 | }
90 |
91 | public void onListItemClick(ListItem selectedItem, View viewRoot){
92 | view.startDetailActivity(
93 | selectedItem.getDateAndTime(),
94 | selectedItem.getMessage(),
95 | selectedItem.getColorResource(),
96 | viewRoot
97 | );
98 | }
99 |
100 | /**
101 | * In a real App, I would normally talk to this DataSource using RxJava 2. This is because most
102 | * calls to Services like a Database/Server should be executed on a seperate thread that the
103 | * mainThread (UI Thread). See my full projects for examples of this.
104 | */
105 | public void getListFromDataSource(){
106 | view.setUpAdapterAndView(
107 | dataSource.getListOfData()
108 | );
109 | }
110 |
111 |
112 | public void createNewListItem() {
113 | /*
114 | To simulate telling the DataSource to create a new record and waiting for it's response,
115 | we'll simply have it return a new ListItem.
116 |
117 | In a real App, I'd use RxJava 2 (or some other
118 | API/Framework for Asynchronous Communication) to have the Datasource do this on the
119 | IO thread, and respond via an Asynchronous callback to the Main thread.
120 | */
121 |
122 | ListItem newItem = dataSource.createNewListItem();
123 |
124 | view.addNewListItemToView(newItem);
125 | }
126 |
127 | public void onListItemSwiped(int position, ListItem listItem) {
128 | //ensure that the view and data layers have consistent state
129 | dataSource.deleteListItem(listItem);
130 | view.deleteListItemAt(position);
131 |
132 | temporaryListItemPosition = position;
133 | temporaryListItem = listItem;
134 |
135 | view.showUndoSnackbar();
136 |
137 | }
138 |
139 | public void onUndoConfirmed() {
140 | if (temporaryListItem != null){
141 | //ensure View/Data consistency
142 | dataSource.insertListItem(temporaryListItem);
143 | view.insertListItemAt(temporaryListItemPosition, temporaryListItem);
144 |
145 | temporaryListItem = null;
146 | temporaryListItemPosition = 0;
147 |
148 | } else {
149 |
150 | }
151 |
152 | }
153 |
154 | public void onSnackbarTimeout() {
155 | temporaryListItem = null;
156 | temporaryListItemPosition = 0;
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/app/src/main/java/forgettery/wiseass/com/recyclerviewprebuilt/view/DetailActivity.java:
--------------------------------------------------------------------------------
1 | /*
2 | * *
3 | * * Copyright (C) 2017 Ryan Kay Open Source Project
4 | * *
5 | * * Licensed under the Apache License, Version 2.0 (the "License");
6 | * * you may not use this file except in compliance with the License.
7 | * * You may obtain a copy of the License at
8 | * *
9 | * * http://www.apache.org/licenses/LICENSE-2.0
10 | * *
11 | * * Unless required by applicable law or agreed to in writing, software
12 | * * distributed under the License is distributed on an "AS IS" BASIS,
13 | * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * * See the License for the specific language governing permissions and
15 | * * limitations under the License.
16 | *
17 | */
18 |
19 | package forgettery.wiseass.com.recyclerviewprebuilt.view;
20 |
21 | import android.content.Intent;
22 | import android.os.Bundle;
23 | import android.support.v4.content.ContextCompat;
24 | import android.support.v7.app.AppCompatActivity;
25 | import android.view.View;
26 | import android.widget.TextView;
27 |
28 | import forgettery.wiseass.com.recyclerviewprebuilt.R;
29 |
30 | /**
31 | * 18.
32 | *
33 | * This Activity simply displays Data from a ListItem in a "Detailed" View.
34 | * Although I'd still give it it's own Presenter/Controller/Whatever in a real App, I won't bother
35 | * doing that here since the Activity is so simplistic anyway.
36 | * Created by R_KAY on 6/3/2017.
37 | */
38 |
39 | public class DetailActivity extends AppCompatActivity {
40 |
41 | private static final String EXTRA_DATE_AND_TIME = "EXTRA_DATE_AND_TIME";
42 | private static final String EXTRA_MESSAGE = "EXTRA_MESSAGE";
43 | private static final String EXTRA_DRAWABLE = "EXTRA_DRAWABLE";
44 |
45 | private TextView dateAndTime;
46 | private TextView message;
47 | private View coloredBackground;
48 |
49 | @Override
50 | protected void onCreate(Bundle savedInstanceState) {
51 | super.onCreate(savedInstanceState);
52 | setContentView(R.layout.activity_detail);
53 |
54 | /*I wouldn't normally pass all this Data via Intent, so understand that this is just a quick
55 | implementation to get things working for the Demo. I'd normally pass just a Unique id as an
56 | extra, and then retrieve the appropriate Data from a Service.*/
57 | Intent i = getIntent();
58 | String dateAndTimeExtra = i.getStringExtra(EXTRA_DATE_AND_TIME);
59 | String messageExtra = i.getStringExtra(EXTRA_MESSAGE);
60 | int drawableResourceExtra = i.getIntExtra(EXTRA_DRAWABLE, 0);
61 |
62 | dateAndTime = (TextView) findViewById(R.id.lbl_date_and_time_header);
63 | dateAndTime.setText(dateAndTimeExtra);
64 |
65 | message = (TextView) findViewById(R.id.lbl_message_body);
66 | message.setText(messageExtra);
67 |
68 | coloredBackground = findViewById(R.id.imv_colored_background);
69 | coloredBackground.setBackgroundResource(
70 | drawableResourceExtra
71 | );
72 |
73 |
74 |
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/java/forgettery/wiseass/com/recyclerviewprebuilt/view/ListActivity.java:
--------------------------------------------------------------------------------
1 | /*
2 | * *
3 | * * Copyright (C) 2017 Ryan Kay Open Source Project
4 | * *
5 | * * Licensed under the Apache License, Version 2.0 (the "License");
6 | * * you may not use this file except in compliance with the License.
7 | * * You may obtain a copy of the License at
8 | * *
9 | * * http://www.apache.org/licenses/LICENSE-2.0
10 | * *
11 | * * Unless required by applicable law or agreed to in writing, software
12 | * * distributed under the License is distributed on an "AS IS" BASIS,
13 | * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * * See the License for the specific language governing permissions and
15 | * * limitations under the License.
16 | *
17 | */
18 |
19 | package forgettery.wiseass.com.recyclerviewprebuilt.view;
20 |
21 | import android.app.ActivityOptions;
22 | import android.content.Intent;
23 | import android.os.Build;
24 | import android.os.Bundle;
25 | import android.support.design.widget.BaseTransientBottomBar;
26 | import android.support.design.widget.FloatingActionButton;
27 | import android.support.design.widget.Snackbar;
28 | import android.support.v4.content.ContextCompat;
29 | import android.support.v7.app.AppCompatActivity;
30 | import android.support.v7.widget.DividerItemDecoration;
31 | import android.support.v7.widget.LinearLayoutManager;
32 | import android.support.v7.widget.RecyclerView;
33 | import android.support.v7.widget.Toolbar;
34 | import android.support.v7.widget.helper.ItemTouchHelper;
35 | import android.transition.Fade;
36 | import android.util.Pair;
37 | import android.view.LayoutInflater;
38 | import android.view.View;
39 | import android.view.ViewGroup;
40 | import android.widget.ProgressBar;
41 | import android.widget.TextView;
42 |
43 | import java.util.List;
44 |
45 | import de.hdodenhof.circleimageview.CircleImageView;
46 | import forgettery.wiseass.com.recyclerviewprebuilt.R;
47 | import forgettery.wiseass.com.recyclerviewprebuilt.data.FakeDataSource;
48 | import forgettery.wiseass.com.recyclerviewprebuilt.data.ListItem;
49 | import forgettery.wiseass.com.recyclerviewprebuilt.logic.Controller;
50 |
51 | /**
52 | * 1.
53 | * List Activity is responsible for
54 | * - Coordinating the User Interface
55 | * - Relaying Click events to the Controller
56 | * - Starting a Detail Activity
57 | * -
58 | */
59 | public class ListActivity extends AppCompatActivity implements ViewInterface, View.OnClickListener {
60 |
61 | private static final String EXTRA_DATE_AND_TIME = "EXTRA_DATE_AND_TIME";
62 | private static final String EXTRA_MESSAGE = "EXTRA_MESSAGE";
63 | private static final String EXTRA_DRAWABLE = "EXTRA_DRAWABLE";
64 |
65 | /**
66 | * 2.
67 | * Obviously you wouldn't use such an ambiguous name in a non-demo App.
68 | */
69 | private List listOfData;
70 |
71 | //12. In order to create each ViewHolder in the UI, we need a LayoutInflater.
72 | private LayoutInflater layoutInflater;
73 | private RecyclerView recyclerView;
74 | private CustomAdapter adapter;
75 | private Toolbar toolbar;
76 |
77 | private Controller controller;
78 |
79 | @Override
80 | protected void onCreate(Bundle savedInstanceState) {
81 | super.onCreate(savedInstanceState);
82 | setContentView(R.layout.activity_list);
83 |
84 | recyclerView = (RecyclerView) findViewById(R.id.rec_list_activity);
85 | layoutInflater = getLayoutInflater();
86 | toolbar = (Toolbar) findViewById(R.id.tlb_list_activity);
87 |
88 | toolbar.setTitle(R.string.title_toolbar);
89 | toolbar.setLogo(R.drawable.ic_view_list_white_24dp);
90 | toolbar.setTitleMarginStart(72);
91 |
92 | FloatingActionButton fabulous = (FloatingActionButton) findViewById(R.id.fab_create_new_item);
93 |
94 | fabulous.setOnClickListener(this);
95 |
96 | controller = new Controller(this, new FakeDataSource());
97 | }
98 |
99 | /**
100 | * 17.
101 | * So, I'd normally just pass an Item's Unique ID (Key) to the other Activity, and then fetch
102 | * the Item from the Database their. However, this is a RecyclerView Demo App and I'm going to
103 | * simplify things like this. Also, by decomposing ListItem, it saves me having to make ListItem
104 | * Parcelable and bla bla bla whatever.
105 | *
106 | * @param dateAndTime
107 | * @param message
108 | * @param colorResource
109 | */
110 | @Override
111 | public void startDetailActivity(String dateAndTime, String message, int colorResource, View viewRoot) {
112 | Intent i = new Intent(this, DetailActivity.class);
113 | i.putExtra(EXTRA_DATE_AND_TIME, dateAndTime);
114 | i.putExtra(EXTRA_MESSAGE, message);
115 | i.putExtra(EXTRA_DRAWABLE, colorResource);
116 |
117 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
118 | getWindow().setEnterTransition(new Fade(Fade.IN));
119 | getWindow().setEnterTransition(new Fade(Fade.OUT));
120 |
121 | ActivityOptions options = ActivityOptions
122 | .makeSceneTransitionAnimation(this,
123 | new Pair(viewRoot.findViewById(R.id.imv_list_item_circle),
124 | getString(R.string.transition_drawable)),
125 | new Pair(viewRoot.findViewById(R.id.lbl_message),
126 | getString(R.string.transition_message)),
127 | new Pair(viewRoot.findViewById(R.id.lbl_date_and_time),
128 | getString(R.string.transition_time_and_date)));
129 |
130 | startActivity(i, options.toBundle());
131 |
132 |
133 | } else {
134 | startActivity(i);
135 | }
136 | }
137 |
138 |
139 | /**
140 | * In order to make sure things execute in the proper order, we have our Controller tell the
141 | * View when to set up it's stuff.
142 | *
143 | * @param listOfData
144 | */
145 | @Override
146 | public void setUpAdapterAndView(List listOfData) {
147 | this.listOfData = listOfData;
148 | LinearLayoutManager layoutManager = new LinearLayoutManager(this);
149 |
150 | recyclerView.setLayoutManager(layoutManager);
151 |
152 | adapter = new CustomAdapter();
153 | recyclerView.setAdapter(adapter);
154 |
155 | DividerItemDecoration itemDecoration = new DividerItemDecoration(
156 | recyclerView.getContext(),
157 | layoutManager.getOrientation()
158 | );
159 |
160 | itemDecoration.setDrawable(
161 | ContextCompat.getDrawable(
162 | ListActivity.this,
163 | R.drawable.divider_white
164 | )
165 | );
166 |
167 | recyclerView.addItemDecoration(
168 | itemDecoration
169 | );
170 |
171 | ItemTouchHelper itemTouchHelper = new ItemTouchHelper(createHelperCallback());
172 | itemTouchHelper.attachToRecyclerView(recyclerView);
173 |
174 |
175 | }
176 |
177 | @Override
178 | public void addNewListItemToView(ListItem newItem) {
179 | listOfData.add(newItem);
180 |
181 | int endOfList = listOfData.size() - 1;
182 |
183 | adapter.notifyItemInserted(endOfList);
184 |
185 | recyclerView.smoothScrollToPosition(endOfList);
186 | }
187 |
188 | @Override
189 | public void deleteListItemAt(int position) {
190 | listOfData.remove(position);
191 |
192 | adapter.notifyItemRemoved(position);
193 | }
194 |
195 | @Override
196 | public void showUndoSnackbar() {
197 | Snackbar.make(
198 | findViewById(R.id.root_list_activity),
199 | getString(R.string.action_delete_item),
200 | Snackbar.LENGTH_LONG
201 | )
202 | .setAction(R.string.action_undo, new View.OnClickListener() {
203 | @Override
204 | public void onClick(View v) {
205 | controller.onUndoConfirmed();
206 | }
207 | })
208 | .addCallback(new BaseTransientBottomBar.BaseCallback() {
209 | @Override
210 | public void onDismissed(Snackbar transientBottomBar, int event) {
211 | super.onDismissed(transientBottomBar, event);
212 |
213 | controller.onSnackbarTimeout();
214 | }
215 | })
216 | .show();
217 | }
218 |
219 | @Override
220 | public void insertListItemAt(int position, ListItem listItem) {
221 | listOfData.add(position, listItem);
222 |
223 | adapter.notifyItemInserted(position);
224 | }
225 |
226 | @Override
227 | public void onClick(View v) {
228 | int viewId = v.getId();
229 | if (viewId == R.id.fab_create_new_item) {
230 | //User wishes to creat a new RecyclerView Item
231 | controller.createNewListItem();
232 | }
233 | }
234 |
235 | /**
236 | * 4.
237 | * (Opinion)
238 | * Why is the Adapter inside the Activity (a.k.a. a Nested Class)?
239 | *
240 | * I used to pull this Class outside of the Activity and have it communicate back to the
241 | * Activity via Interface. This was actually detrimental in retrospect, as I ended up having to
242 | * manage a List of Data in the Activity, as well as a copy List of Data in the Adapter itself.
243 | *
244 | * Also, since I needed to have my ViewHolder implement an OnClickListener, and then have the
245 | * result of that talk to the Activity through another interface, it was pretty confusing to
246 | * Beginners (not to mention pointless).
247 | *
248 | * That being said, you don't *have* to nest the Adapter. Do what makes sense for the Software
249 | * Architecture in front of you.
250 | */
251 | private class CustomAdapter extends RecyclerView.Adapter {//6
252 |
253 | /**
254 | * 13.
255 | * Inflates a new View (in this case, R.layout.item_data), and then creates/returns a new
256 | * CustomViewHolder object.
257 | *
258 | * @param parent Unfortunately the docs currently don't explain this at all :(
259 | * @param viewType Unfortunately the docs currently don't explain this at all :(
260 | * @return
261 | */
262 | @Override
263 | public CustomAdapter.CustomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
264 | View v = layoutInflater.inflate(R.layout.item_data, parent, false);
265 | return new CustomViewHolder(v);
266 | }
267 |
268 | /**
269 | * This method "Binds" or assigns Data (from listOfData) to each View (ViewHolder).
270 | *
271 | * @param holder The current ViewHolder instance for a given position
272 | * @param position The current position of the ViewHolder we are Binding to, based upon
273 | * our (listOfData). So for the second ViewHolder we create, we'll bind data
274 | * from the second Item in listOfData.
275 | */
276 | @Override
277 | public void onBindViewHolder(CustomAdapter.CustomViewHolder holder, int position) {
278 | //11. and now the ViewHolder data
279 | ListItem currentItem = listOfData.get(position);
280 |
281 | holder.coloredCircle.setImageResource(
282 | currentItem.getColorResource()
283 | );
284 |
285 | holder.message.setText(
286 | currentItem.getMessage()
287 | );
288 |
289 | holder.dateAndTime.setText(
290 | currentItem.getDateAndTime()
291 | );
292 |
293 | holder.loading.setVisibility(View.INVISIBLE);
294 | }
295 |
296 | /**
297 | * This method let's our Adapter determine how many ViewHolders it needs to create, based on
298 | * the size of the Dataset (List) which it is working with.
299 | *
300 | * @return the size of the dataset, generally via List.size()
301 | */
302 | @Override
303 | public int getItemCount() {
304 | // 12. Returning 0 here will tell our Adapter not to make any Items. Let's fix that.
305 | return listOfData.size();
306 | }
307 |
308 | /**
309 | * 5.
310 | * Each ViewHolder contains Bindings to the Views we wish to populate with Data.
311 | */
312 | class CustomViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
313 |
314 | //10. now that we've made our layouts, let's bind them
315 | private CircleImageView coloredCircle;
316 | private TextView dateAndTime;
317 | private TextView message;
318 | private ViewGroup container;
319 | private ProgressBar loading;
320 |
321 | public CustomViewHolder(View itemView) {
322 | super(itemView);
323 | this.coloredCircle = (CircleImageView) itemView.findViewById(R.id.imv_list_item_circle);
324 | this.dateAndTime = (TextView) itemView.findViewById(R.id.lbl_date_and_time);
325 | this.message = (TextView) itemView.findViewById(R.id.lbl_message);
326 | this.loading = (ProgressBar) itemView.findViewById(R.id.pro_item_data);
327 |
328 | this.container = (ViewGroup) itemView.findViewById(R.id.root_list_item);
329 | /*
330 | We can pass "this" as an Argument, because "this", which refers to the Current
331 | Instance of type CustomViewHolder currently conforms to (implements) the
332 | View.OnClickListener interface. I have a Video on my channel which goes into
333 | Interfaces with Detailed Examples.
334 |
335 | Search "Android WTF: Java Interfaces by Example"
336 | */
337 | this.container.setOnClickListener(this);
338 | }
339 |
340 | /**
341 | * 6.
342 | * Since I'm ok with the whole Container being the Listener, View v isn't super useful
343 | * in this Use Case. However, if I had a Single RecyclerView Item with multiple
344 | * Clickable Views, I could use v.getId() to tell which specific View was clicked.
345 | * See the comment within the method.
346 | *
347 | * @param v
348 | */
349 | @Override
350 | public void onClick(View v) {
351 | //getAdapterPosition() get's an Integer based on which the position of the current
352 | //ViewHolder (this) in the Adapter. This is how we get the correct Data.
353 | ListItem listItem = listOfData.get(
354 | this.getAdapterPosition()
355 | );
356 |
357 | controller.onListItemClick(
358 | listItem,
359 | v
360 | );
361 |
362 | }
363 | }
364 | }
365 |
366 | private ItemTouchHelper.Callback createHelperCallback() {
367 | /*First Param is for Up/Down motion, second is for Left/Right.
368 | Note that we can supply 0, one constant (e.g. ItemTouchHelper.LEFT), or two constants (e.g.
369 | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) to specify what directions are allowed.
370 | */
371 | ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(
372 | 0,
373 | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
374 |
375 | //not used, as the first parameter above is 0
376 | @Override
377 | public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
378 | RecyclerView.ViewHolder target) {
379 | return false;
380 | }
381 |
382 |
383 | @Override
384 | public void onSwiped(final RecyclerView.ViewHolder viewHolder, int swipeDir) {
385 | int position = viewHolder.getAdapterPosition();
386 | controller.onListItemSwiped(
387 | position,
388 | listOfData.get(position)
389 | );
390 | }
391 | };
392 |
393 | return simpleItemTouchCallback;
394 | }
395 | }
396 |
--------------------------------------------------------------------------------
/app/src/main/java/forgettery/wiseass/com/recyclerviewprebuilt/view/ViewInterface.java:
--------------------------------------------------------------------------------
1 | /*
2 | * *
3 | * * Copyright (C) 2017 Ryan Kay Open Source Project
4 | * *
5 | * * Licensed under the Apache License, Version 2.0 (the "License");
6 | * * you may not use this file except in compliance with the License.
7 | * * You may obtain a copy of the License at
8 | * *
9 | * * http://www.apache.org/licenses/LICENSE-2.0
10 | * *
11 | * * Unless required by applicable law or agreed to in writing, software
12 | * * distributed under the License is distributed on an "AS IS" BASIS,
13 | * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * * See the License for the specific language governing permissions and
15 | * * limitations under the License.
16 | *
17 | */
18 |
19 | package forgettery.wiseass.com.recyclerviewprebuilt.view;
20 |
21 | import android.view.View;
22 |
23 | import java.util.List;
24 |
25 | import forgettery.wiseass.com.recyclerviewprebuilt.data.ListItem;
26 |
27 | /**
28 | * This Interface is the Contract which dictates how our View and Presenter can talk to each other.
29 | *
30 | * @see Android Java Interfaces by Example
31 | * Created by R_KAY on 6/3/2017.
32 | */
33 |
34 | public interface ViewInterface {
35 |
36 | void startDetailActivity(String dateAndTime, String message, int colorResource, View viewRoot);
37 |
38 | void setUpAdapterAndView(List listOfData);
39 |
40 | void addNewListItemToView(ListItem newItem);
41 |
42 | void deleteListItemAt(int position);
43 |
44 | void showUndoSnackbar();
45 |
46 | void insertListItemAt(int temporaryListItemPosition, ListItem temporaryListItem);
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_add_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-hdpi/ic_add_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_event_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-hdpi/ic_event_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_event_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-hdpi/ic_event_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_view_list_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-hdpi/ic_view_list_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_add_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-mdpi/ic_add_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_event_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-mdpi/ic_event_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_event_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-mdpi/ic_event_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_view_list_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-mdpi/ic_view_list_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_event_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-xhdpi/ic_event_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_event_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-xhdpi/ic_event_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_view_list_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-xhdpi/ic_view_list_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_event_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-xxhdpi/ic_event_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_event_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-xxhdpi/ic_event_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_add_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-xxxhdpi/ic_add_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_event_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-xxxhdpi/ic_event_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_event_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-xxxhdpi/ic_event_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_view_list_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/drawable-xxxhdpi/ic_view_list_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/blue_drawable.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/divider_white.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/green_drawable.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/red_drawable.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/yellow_drawable.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
26 |
27 |
28 |
34 |
35 |
36 |
46 |
47 |
55 |
56 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
27 |
28 |
31 |
32 |
43 |
44 |
58 |
59 |
69 |
70 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_data.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
29 |
30 |
43 |
44 |
57 |
58 |
78 |
79 |
93 |
94 |
107 |
108 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 | #121212
22 | #000000
23 | #80D8FF
24 | #121212
25 |
26 |
27 | #52000000
28 | #52FFFFFF
29 |
30 | #FF0000
31 | #0000FF
32 | #00FF00
33 | #FFEB3B
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 | RecyclerViewPrebuilt
21 | Item Deleted
22 | UNDO?
23 |
24 |
25 | drawable
26 | message
27 | date
28 | List Activity
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
28 |
29 |
30 |
34 |
35 |
38 |
39 |
43 |
44 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/test/java/forgettery/wiseass/com/recyclerviewprebuilt/ControllerUnitTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * *
3 | * * Copyright (C) 2017 Ryan Kay Open Source Project
4 | * *
5 | * * Licensed under the Apache License, Version 2.0 (the "License");
6 | * * you may not use this file except in compliance with the License.
7 | * * You may obtain a copy of the License at
8 | * *
9 | * * http://www.apache.org/licenses/LICENSE-2.0
10 | * *
11 | * * Unless required by applicable law or agreed to in writing, software
12 | * * distributed under the License is distributed on an "AS IS" BASIS,
13 | * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * * See the License for the specific language governing permissions and
15 | * * limitations under the License.
16 | *
17 | */
18 |
19 | package forgettery.wiseass.com.recyclerviewprebuilt;
20 |
21 | import android.view.View;
22 |
23 | import org.junit.Before;
24 | import org.junit.Test;
25 | import org.junit.runner.RunWith;
26 | import org.mockito.Mock;
27 | import org.mockito.Mockito;
28 | import org.mockito.junit.MockitoJUnitRunner;
29 |
30 | import java.util.ArrayList;
31 | import java.util.List;
32 |
33 | import forgettery.wiseass.com.recyclerviewprebuilt.data.DataSourceInterface;
34 | import forgettery.wiseass.com.recyclerviewprebuilt.data.ListItem;
35 | import forgettery.wiseass.com.recyclerviewprebuilt.logic.Controller;
36 | import forgettery.wiseass.com.recyclerviewprebuilt.view.ViewInterface;
37 |
38 | /**
39 | * Test Driven Development Bonus: Before writing the methods themselves (a.k.a. writing the
40 | * "Implementation"), I've written some Unit Tests. In a nutshell, these Tests help me to figure out
41 | * what methods and logic I'll need for the class which I'm testing. This process is slow at first,
42 | * but once you become fast at writing Tests, you'll start to see how they actually help you write
43 | * Better Code, often at a Faster Pace.
44 | *
45 | * I have some Videos on this topic on my youtube channel. Check them out for more info.
46 | */
47 | @RunWith(MockitoJUnitRunner.class)
48 | public class ControllerUnitTest {
49 |
50 | //We technically could've just used the FakeDataSource here, but you don't always want to use
51 | //Mocks over Fakes and vice versa. Depends on your use case.
52 | @Mock
53 | DataSourceInterface dataSource;
54 |
55 | @Mock
56 | ViewInterface view;
57 |
58 | @Mock
59 | View testViewRoot;
60 |
61 | /**
62 | * Often times we'll want data to run our tests against. I like to define such Data as a series
63 | * of Static variables in the Test file itself for convenience.
64 | */
65 | private static final ListItem TEST_ITEM = new ListItem(
66 | "6:30AM 06/01/2017",
67 | "Look at Open Source Projects",
68 | R.drawable.red_drawable);
69 |
70 |
71 | private static final int POSITION = 3;
72 |
73 | /**
74 | * Since we're testing our Controller Class, this one is the Real implementation!
75 | * How this works is we make a Real instance of the Controller talk to Fake Instances of
76 | * what it would normally talk to (i.e. our Mocks above). Since the Controller does most of the
77 | * thinking (Logic), it follows that it would benefit the most from a good suite of Tests.
78 | */
79 | Controller controller;
80 |
81 | /**
82 | * the @Before annotation makes this Code run before the Tests themselves. You can use this
83 | * to initialize the different components of your Test, should you need to. You can also
84 | * initialize things in the Test methods themselves. Whatever works best for your Use Case.
85 | */
86 | @Before
87 | public void setUpTest() {
88 | controller = new Controller(view, dataSource);
89 | }
90 |
91 | @Test
92 | public void onGetListDataSuccessful() {
93 | //Since we require a List to be returned by the dataSource, we can define it here:
94 | List listOfData = new ArrayList<>();
95 | listOfData.add(TEST_ITEM);
96 |
97 |
98 | //This is where we tell our "Mocks" what to do when our Controller talks to them. Since they
99 | //aren't real objects, we must tell them exactly what to do if we want responses from them.
100 | Mockito.when(dataSource.getListOfData())
101 | .thenReturn(listOfData);
102 |
103 | //This is the method we are testing
104 | controller.getListFromDataSource();
105 |
106 | //Check how the Tested Class responds to the data it receives
107 | //or test it's behaviour
108 | Mockito.verify(view).setUpAdapterAndView(listOfData);
109 | }
110 |
111 |
112 |
113 | @Test
114 | public void onListItemClicked() {
115 | controller.onListItemClick(TEST_ITEM, testViewRoot);
116 |
117 | Mockito.verify(view).startDetailActivity(
118 | TEST_ITEM.getDateAndTime(),
119 | TEST_ITEM.getMessage(),
120 | TEST_ITEM.getColorResource(),
121 | testViewRoot);
122 | }
123 |
124 |
125 | // @Test
126 | // public void onGetListDataUnsuccessful() {
127 | /**************************
128 | *
129 | * Unit Test Homework:
130 | *
131 | * Implement the "View", so that when we don't recieve a List, it shows some kind of
132 | * error message to the user. This is common practice that you should learn!
133 | *
134 | * I've written some hints you'll have to decipher into Java code:
135 | *************************/
136 | //1 Set up your Mock dataSource
137 |
138 | //2 Call the method you wish to test on the Controller
139 |
140 | //3 Verify that the View has been told to show a message (I'd suggest showing a Toast for now)
141 |
142 | //Profit???
143 |
144 | // }
145 |
146 | @Test
147 | public void onCreateNewListItemClick() {
148 | //1 Set up your Mock dataSource
149 | Mockito.when(dataSource.createNewListItem())
150 | .thenReturn(TEST_ITEM);
151 |
152 | //2 Call the method you wish to test on the Controller
153 | controller.createNewListItem();
154 |
155 | //3 Verify the behaviour of the View, based on the event
156 | Mockito.verify(view).addNewListItemToView(
157 | TEST_ITEM
158 | );
159 | }
160 |
161 | /**
162 | * User has swiped to delete an Item from the List. Since this operation may be accidental, it
163 | * is necessary to provide a way for the User to Undo this operation. For now, we will
164 | * 1. Temporarily store ListItem and position in adapter
165 | * 2. Tell the Datasource to delete the Item (this ensures view/data layer consistency as much as
166 | * possible)
167 | *
168 | * After that, the user will either undo, or the snackbar dialog will timeout (see tests below)
169 | */
170 | @Test
171 | public void onListItemSwiped() {
172 |
173 | controller.onListItemSwiped(POSITION, TEST_ITEM);
174 |
175 | //ensure consistency between View and Data Layers
176 | Mockito.verify(dataSource).deleteListItem(TEST_ITEM);
177 | Mockito.verify(view).deleteListItemAt(POSITION);
178 |
179 | //give user the option to undo action
180 | Mockito.verify(view).showUndoSnackbar();
181 |
182 | }
183 |
184 |
185 | /**
186 | * When the User Undoes delete operation, we must do the following:
187 | * - Add the temp item back into the Datasource
188 | * - Animate the item back into the View
189 | */
190 | @Test
191 | public void onUndoDeleteOperation() {
192 | Mockito.when(dataSource.createNewListItem())
193 | .thenReturn(TEST_ITEM);
194 |
195 | //this test requires temporary position and item to be set. We can achieve this by calling
196 | //controller.onListItemSwiped() first.
197 | controller.onListItemSwiped(POSITION, TEST_ITEM);
198 |
199 | controller.onUndoConfirmed();
200 |
201 | Mockito.verify(dataSource).insertListItem(TEST_ITEM);
202 |
203 | Mockito.verify(view).insertListItemAt(
204 | POSITION,
205 | TEST_ITEM
206 | );
207 | }
208 |
209 | /**
210 | * Since the only interaction with Controller is from methods called on it within the Test,
211 | * there isn't really anything to verify against. However, we could use the Debugger to step
212 | * through the Test if necessary.
213 | *
214 | * ¯\_(ツ)_/¯
215 | */
216 | @Test
217 | public void onSnackbarTimeout() {
218 | controller.onListItemSwiped(POSITION, TEST_ITEM);
219 |
220 | controller.onSnackbarTimeout();
221 | }
222 |
223 |
224 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.3.3'
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 |
21 | task clean(type: Delete) {
22 | delete rootProject.buildDir
23 | }
24 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BracketCove/RecyclerViewTutorial2017/5d2234992993cf92600b2dff4494e010aec0d756/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed May 31 18:49:52 PDT 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------