modelClass) {
36 | if (modelClass.isAssignableFrom(DeleteCompletedItemsViewModel.class)) {
37 | return (T) new DeleteCompletedItemsViewModel(application,
38 | loadShoppingListUseCase,
39 | updateShoppingListUseCase,
40 | schedulersFacade,
41 | shoppingListDataHelper);
42 | }
43 | throw new IllegalArgumentException("Unknown ViewModel class");
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jshvarts/shoppinglist/common/viewmodel/SingleLiveEvent.java:
--------------------------------------------------------------------------------
1 | package com.jshvarts.shoppinglist.common.viewmodel;
2 |
3 | import android.arch.lifecycle.LifecycleOwner;
4 | import android.arch.lifecycle.MutableLiveData;
5 | import android.arch.lifecycle.Observer;
6 | import android.support.annotation.MainThread;
7 | import android.support.annotation.Nullable;
8 |
9 | import java.util.concurrent.atomic.AtomicBoolean;
10 |
11 | import timber.log.Timber;
12 |
13 | /**
14 | * A lifecycle-aware observable that sends only new updates after subscription, used for events like
15 | * navigation and Snackbar messages.
16 | *
17 | * This avoids a common problem with events: on configuration change (like rotation) an update
18 | * can be emitted if the observer is active. This LiveData only calls the observable if there's an
19 | * explicit call to setValue() or call().
20 | *
21 | * Note that only one observer is going to be notified of changes.
22 | *
23 | * Source https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java
24 | */
25 | public class SingleLiveEvent extends MutableLiveData {
26 |
27 | private static final String TAG = "SingleLiveEvent";
28 |
29 | private final AtomicBoolean mPending = new AtomicBoolean(false);
30 |
31 | @MainThread
32 | public void observe(LifecycleOwner owner, final Observer observer) {
33 |
34 | if (hasActiveObservers()) {
35 | Timber.w("Multiple observers registered but only one will be notified of changes.");
36 | }
37 |
38 | // Observe the internal MutableLiveData
39 | super.observe(owner, t -> {
40 | if (mPending.compareAndSet(true, false)) {
41 | observer.onChanged(t);
42 | }
43 | });
44 | }
45 |
46 | @MainThread
47 | public void setValue(@Nullable T t) {
48 | mPending.set(true);
49 | super.setValue(t);
50 | }
51 |
52 | /**
53 | * Used for cases where T is Void, to make calls cleaner.
54 | */
55 | @MainThread
56 | public void call() {
57 | setValue(null);
58 | }
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jshvarts/shoppinglist/lobby/fragments/ShoppingListAdapter.java:
--------------------------------------------------------------------------------
1 | package com.jshvarts.shoppinglist.lobby.fragments;
2 |
3 | import android.graphics.Color;
4 | import android.graphics.Paint;
5 | import android.support.v7.widget.CardView;
6 | import android.support.v7.widget.RecyclerView;
7 | import android.view.LayoutInflater;
8 | import android.view.ViewGroup;
9 | import android.widget.TextView;
10 |
11 | import com.jshvarts.shoppinglist.R;
12 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingListItem;
13 |
14 | import java.util.List;
15 |
16 | public class ShoppingListAdapter extends RecyclerView.Adapter {
17 |
18 | private final List shoppingListItems;
19 |
20 | public ShoppingListAdapter(List shoppingListItems) {
21 | this.shoppingListItems = shoppingListItems;
22 | }
23 |
24 | @Override
25 | public ShoppingListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
26 | CardView itemContainer = (CardView) LayoutInflater.from(parent.getContext())
27 | .inflate(R.layout.shopping_list_item, parent, false);
28 | return new ViewHolder(itemContainer);
29 | }
30 |
31 | @Override
32 | public void onBindViewHolder(ViewHolder holder, int position) {
33 | final ShoppingListItem shoppingListItem = shoppingListItems.get(position);
34 | if (shoppingListItem.getCompleted()) {
35 | // TODO define style for completed items
36 | holder.itemViewContainer.setCardBackgroundColor(Color.LTGRAY);
37 | holder.itemName.setPaintFlags(Paint.STRIKE_THRU_TEXT_FLAG);
38 | }
39 | holder.itemName.setText(shoppingListItem.getName());
40 | }
41 |
42 | @Override
43 | public int getItemCount() {
44 | return shoppingListItems == null ? 0 : shoppingListItems.size();
45 | }
46 |
47 | /**
48 | * View holder for shopping list items of this adapter
49 | */
50 | public static class ViewHolder extends RecyclerView.ViewHolder {
51 |
52 | private CardView itemViewContainer;
53 |
54 | private TextView itemName;
55 |
56 | public ViewHolder(final CardView itemViewContainer) {
57 | super(itemViewContainer);
58 | this.itemViewContainer = itemViewContainer;
59 | this.itemName = (TextView) itemViewContainer.findViewById(R.id.shopping_list_item_name_textview);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jshvarts/shoppinglist/lobby/LobbyActivity.java:
--------------------------------------------------------------------------------
1 | package com.jshvarts.shoppinglist.lobby;
2 |
3 | import android.os.Bundle;
4 | import android.support.v4.app.Fragment;
5 | import android.support.v7.app.AppCompatActivity;
6 | import android.support.v7.widget.Toolbar;
7 | import android.widget.FrameLayout;
8 |
9 | import com.jshvarts.shoppinglist.R;
10 | import com.jshvarts.shoppinglist.lobby.fragments.ViewShoppingListFragment;
11 |
12 | import javax.inject.Inject;
13 |
14 | import butterknife.BindView;
15 | import butterknife.ButterKnife;
16 | import dagger.android.AndroidInjection;
17 | import dagger.android.AndroidInjector;
18 | import dagger.android.DispatchingAndroidInjector;
19 | import dagger.android.support.HasSupportFragmentInjector;
20 |
21 | public class LobbyActivity extends AppCompatActivity implements HasSupportFragmentInjector {
22 |
23 | @Inject
24 | DispatchingAndroidInjector fragmentDispatchingAndroidInjector;
25 |
26 | @BindView(R.id.toolbar)
27 | Toolbar toolbar;
28 |
29 | @BindView(R.id.fragment_container)
30 | FrameLayout fragmentContainer;
31 |
32 | @Override
33 | protected void onCreate(Bundle savedInstanceState) {
34 | AndroidInjection.inject(this);
35 | super.onCreate(savedInstanceState);
36 | setContentView(R.layout.lobby_activity);
37 |
38 | ButterKnife.bind(this);
39 |
40 | setSupportActionBar(toolbar);
41 |
42 | if (savedInstanceState == null) {
43 | // Activity has not been recreated
44 | attachViewShoppingListFragment();
45 | }
46 | }
47 |
48 | @Override
49 | public void onBackPressed() {
50 | Fragment shoppingListFragment = getSupportFragmentManager().findFragmentByTag(ViewShoppingListFragment.TAG);
51 | if (shoppingListFragment.getChildFragmentManager().getBackStackEntryCount() > 0) {
52 | shoppingListFragment.getChildFragmentManager().popBackStack();
53 | return;
54 | }
55 | finish();
56 | }
57 |
58 | @Override
59 | public AndroidInjector supportFragmentInjector() {
60 | return fragmentDispatchingAndroidInjector;
61 | }
62 |
63 | private void attachViewShoppingListFragment() {
64 | Fragment shoppingListFragment = new ViewShoppingListFragment();
65 | getSupportFragmentManager().beginTransaction()
66 | .add(R.id.fragment_container, shoppingListFragment, ViewShoppingListFragment.TAG)
67 | .addToBackStack(ViewShoppingListFragment.TAG)
68 | .commit();
69 | }
70 |
71 | private boolean isViewShoppingListFragmentAttached() {
72 | return getSupportFragmentManager().findFragmentByTag(ViewShoppingListFragment.TAG) != null;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jshvarts/shoppinglist/lobby/fragments/AddShoppingListItemViewModel.java:
--------------------------------------------------------------------------------
1 | package com.jshvarts.shoppinglist.lobby.fragments;
2 |
3 | import android.arch.lifecycle.ViewModel;
4 |
5 | import com.google.common.base.Strings;
6 | import com.jshvarts.shoppinglist.common.domain.model.DatabaseConstants;
7 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingList;
8 | import com.jshvarts.shoppinglist.common.viewmodel.SingleLiveEvent;
9 | import com.jshvarts.shoppinglist.rx.SchedulersFacade;
10 |
11 | import io.reactivex.disposables.CompositeDisposable;
12 | import timber.log.Timber;
13 |
14 | class AddShoppingListItemViewModel extends ViewModel {
15 |
16 | private final LoadShoppingListUseCase loadShoppingListUseCase;
17 |
18 | private final AddShoppingListItemUseCase addShoppingListItemUseCase;
19 |
20 | private final SchedulersFacade schedulersFacade;
21 |
22 | private final CompositeDisposable disposables = new CompositeDisposable();
23 |
24 | private SingleLiveEvent itemAdded = new SingleLiveEvent<>();
25 |
26 | private SingleLiveEvent itemInvalid = new SingleLiveEvent<>();
27 |
28 | private SingleLiveEvent hideKeyboard = new SingleLiveEvent<>();
29 |
30 | AddShoppingListItemViewModel(AddShoppingListItemUseCase addShoppingListItemUseCase,
31 | LoadShoppingListUseCase loadShoppingListUseCase,
32 | SchedulersFacade schedulersFacade) {
33 | this.addShoppingListItemUseCase = addShoppingListItemUseCase;
34 | this.loadShoppingListUseCase = loadShoppingListUseCase;
35 | this.schedulersFacade = schedulersFacade;
36 | }
37 |
38 | SingleLiveEvent itemAdded() {
39 | return itemAdded;
40 | }
41 |
42 | SingleLiveEvent itemInvalid() {
43 | return itemInvalid;
44 | }
45 |
46 | SingleLiveEvent hideKeyboard() {
47 | return hideKeyboard;
48 | }
49 |
50 | void addShoppingListItem(String shoppingListItemName) {
51 | hideKeyboard.call();
52 |
53 | if (Strings.isNullOrEmpty(shoppingListItemName)) {
54 | itemInvalid.call();
55 | return;
56 | }
57 | loadShoppingListAndAddItem(shoppingListItemName);
58 | }
59 |
60 | private void loadShoppingListAndAddItem(String shoppingListItemName) {
61 | disposables.add(loadShoppingListUseCase.loadShoppingList(DatabaseConstants.DEFAULT_SHOPPING_LIST_ID)
62 | .subscribeOn(schedulersFacade.io())
63 | .observeOn(schedulersFacade.ui())
64 | .firstElement()
65 | .subscribe(shoppingList -> addShoppingListItem(shoppingList, shoppingListItemName),
66 | throwable -> Timber.e(throwable))
67 | );
68 | }
69 |
70 | private void addShoppingListItem(ShoppingList shoppingList, String shoppingListItemName) {
71 | disposables.add(addShoppingListItemUseCase.execute(shoppingList, shoppingListItemName)
72 | .subscribeOn(schedulersFacade.io())
73 | .observeOn(schedulersFacade.ui())
74 | .subscribe(updatedShoppingList -> itemAdded.setValue(true), throwable -> {
75 | Timber.e(throwable);
76 | itemAdded.setValue(false);
77 | }));
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jshvarts/shoppinglist/lobby/fragments/AddShoppingListItemFragment.java:
--------------------------------------------------------------------------------
1 | package com.jshvarts.shoppinglist.lobby.fragments;
2 |
3 | import android.arch.lifecycle.LifecycleFragment;
4 | import android.arch.lifecycle.ViewModelProviders;
5 | import android.content.Context;
6 | import android.os.Bundle;
7 | import android.support.annotation.Nullable;
8 | import android.view.LayoutInflater;
9 | import android.view.View;
10 | import android.view.ViewGroup;
11 | import android.view.inputmethod.InputMethodManager;
12 | import android.widget.EditText;
13 | import android.widget.Toast;
14 |
15 | import com.jshvarts.shoppinglist.R;
16 |
17 | import javax.inject.Inject;
18 |
19 | import butterknife.BindView;
20 | import butterknife.ButterKnife;
21 | import butterknife.OnClick;
22 | import butterknife.Unbinder;
23 | import dagger.android.support.AndroidSupportInjection;
24 |
25 | public class AddShoppingListItemFragment extends LifecycleFragment {
26 |
27 | public static final String TAG = AddShoppingListItemFragment.class.getSimpleName();
28 |
29 | @Inject
30 | AddShoppingListItemViewModelFactory viewModelFactory;
31 |
32 | @BindView(R.id.shopping_list_item_name_edittext)
33 | EditText addShoppingListItemButtonEditText;
34 |
35 | private AddShoppingListItemViewModel viewModel;
36 |
37 | private Unbinder unbinder;
38 |
39 | @Override
40 | public void onAttach(Context context) {
41 | AndroidSupportInjection.inject(this);
42 | super.onAttach(context);
43 | }
44 |
45 | @Nullable
46 | @Override
47 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
48 | View view = inflater.inflate(R.layout.add_shopping_list_item_fragment, container, false);
49 | unbinder = ButterKnife.bind(this, view);
50 | return view;
51 | }
52 |
53 | @Override
54 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
55 | super.onViewCreated(view, savedInstanceState);
56 |
57 | viewModel = ViewModelProviders.of(this, viewModelFactory).get(AddShoppingListItemViewModel.class);
58 |
59 | viewModel.hideKeyboard().observe(this, response -> hideKeyboard());
60 |
61 | viewModel.itemInvalid().observe(this, response ->
62 | Toast.makeText(getActivity(), R.string.create_shopping_list_item_validation_error, Toast.LENGTH_SHORT).show());
63 |
64 | viewModel.itemAdded().observe(this, isSuccess -> handleIsItemAddedResponse(isSuccess));
65 | }
66 |
67 | @OnClick(R.id.save_shopping_list_item_button)
68 | void onSaveShoppingListItemButtonClicked() {
69 | viewModel.addShoppingListItem(addShoppingListItemButtonEditText.getText().toString());
70 | }
71 |
72 | @Override
73 | public void onDestroyView() {
74 | super.onDestroyView();
75 | unbinder.unbind();
76 | }
77 |
78 | private void handleIsItemAddedResponse(boolean isSuccess) {
79 | if (isSuccess) {
80 | detachFragment();
81 | } else {
82 | Toast.makeText(getActivity(), R.string.create_shopping_list_item_error, Toast.LENGTH_SHORT).show();
83 | }
84 | }
85 |
86 | private void hideKeyboard() {
87 | InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
88 | imm.hideSoftInputFromWindow(getView().getWindowToken(), 0);
89 | }
90 |
91 | private void detachFragment() {
92 | getActivity().getSupportFragmentManager().beginTransaction().remove(this).commit();
93 | getParentFragment().getChildFragmentManager().popBackStack();
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jshvarts/shoppinglist/lobby/fragments/ShoppingListViewModel.java:
--------------------------------------------------------------------------------
1 | package com.jshvarts.shoppinglist.lobby.fragments;
2 |
3 | import android.arch.lifecycle.LiveData;
4 | import android.arch.lifecycle.MutableLiveData;
5 | import android.arch.lifecycle.ViewModel;
6 |
7 | import com.jshvarts.shoppinglist.common.domain.model.DatabaseConstants;
8 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingList;
9 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingListDataHelper;
10 | import com.jshvarts.shoppinglist.rx.SchedulersFacade;
11 |
12 | import io.reactivex.disposables.CompositeDisposable;
13 | import timber.log.Timber;
14 |
15 | class ShoppingListViewModel extends ViewModel {
16 |
17 | private final LoadShoppingListUseCase loadShoppingListUseCase;
18 |
19 | private final UpdateShoppingListUseCase updateShoppingListUseCase;
20 |
21 | private final SchedulersFacade schedulersFacade;
22 |
23 | private final ShoppingListDataHelper shoppingListDataHelper;
24 |
25 | private final CompositeDisposable disposables = new CompositeDisposable();
26 |
27 | private MutableLiveData liveShoppingList = new MutableLiveData<>();
28 |
29 | private MutableLiveData loadingIndicatorStatus = new MutableLiveData<>();
30 |
31 | ShoppingListViewModel(LoadShoppingListUseCase loadShoppingListUseCase,
32 | UpdateShoppingListUseCase updateShoppingListUseCase,
33 | SchedulersFacade schedulersFacade,
34 | ShoppingListDataHelper shoppingListDataHelper) {
35 | this.loadShoppingListUseCase = loadShoppingListUseCase;
36 | this.updateShoppingListUseCase = updateShoppingListUseCase;
37 | this.schedulersFacade = schedulersFacade;
38 | this.shoppingListDataHelper = shoppingListDataHelper;
39 | }
40 |
41 | @Override
42 | protected void onCleared() {
43 | disposables.clear();
44 | }
45 |
46 | LiveData getShoppingList() {
47 | return liveShoppingList;
48 | }
49 |
50 | LiveData getLoadingIndicatorStatus() {
51 | return loadingIndicatorStatus;
52 | }
53 |
54 | void loadShoppingList() {
55 | disposables.add(loadShoppingListUseCase.loadShoppingList(DatabaseConstants.DEFAULT_SHOPPING_LIST_ID)
56 | .subscribeOn(schedulersFacade.io())
57 | .observeOn(schedulersFacade.ui())
58 | .doOnSubscribe(s -> loadingIndicatorStatus.setValue(true))
59 | .subscribe(shoppingList -> {
60 | liveShoppingList.setValue(shoppingList);
61 | loadingIndicatorStatus.setValue(false);
62 | }, throwable -> {
63 | Timber.e(throwable);
64 | loadingIndicatorStatus.setValue(false);
65 | }
66 | ));
67 | }
68 |
69 | void completeShoppingListItem(int itemIndex) {
70 | ShoppingList shoppingList = liveShoppingList.getValue();
71 | if (shoppingList.getItems().get(itemIndex).getCompleted()) {
72 | // item already completed. trigger UI refresh only.
73 | liveShoppingList.setValue(shoppingList);
74 | return;
75 | }
76 | shoppingListDataHelper.completeItem(shoppingList, itemIndex);
77 |
78 | disposables.add(updateShoppingListUseCase.updateShoppingList(shoppingList)
79 | .subscribeOn(schedulersFacade.io())
80 | .observeOn(schedulersFacade.ui())
81 | .subscribe(updatedShoppingList -> liveShoppingList.setValue(updatedShoppingList),
82 | throwable -> Timber.e(throwable)
83 | )
84 | );
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jshvarts/shoppinglist/common/domain/model/stub/StubShoppingListRepository.java:
--------------------------------------------------------------------------------
1 | package com.jshvarts.shoppinglist.common.domain.model.stub;
2 |
3 | import com.jshvarts.shoppinglist.common.domain.model.Repository;
4 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingList;
5 | import com.jshvarts.shoppinglist.common.domain.model.ItemByIdSpecification;
6 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingListItem;
7 | import com.jshvarts.shoppinglist.common.domain.model.Specification;
8 |
9 | import java.util.ArrayList;
10 | import java.util.HashSet;
11 | import java.util.List;
12 | import java.util.Set;
13 |
14 | import javax.inject.Singleton;
15 |
16 | import io.reactivex.Completable;
17 | import io.reactivex.Observable;
18 | import io.reactivex.Single;
19 | import timber.log.Timber;
20 |
21 | @Singleton
22 | public class StubShoppingListRepository implements Repository {
23 |
24 | private final Set shoppingLists;
25 |
26 | public StubShoppingListRepository() {
27 | this.shoppingLists = new HashSet<>();
28 | initializeShoppingLists();
29 | }
30 |
31 | @Override
32 | public Single> getItems(Specification specification) {
33 | //ItemsSpecification itemsSpecification = (ItemsSpecification) specification;
34 | return Single.just(new ArrayList<>(shoppingLists));
35 | }
36 |
37 | @Override
38 | public Observable getItem(Specification specification) {
39 | ItemByIdSpecification byIdSpecification = (ItemByIdSpecification) specification;
40 | for (ShoppingList shoppingList : shoppingLists) {
41 | if (shoppingList.getId().equals(byIdSpecification.getId())) {
42 | Timber.d("found shopping list that matches " + byIdSpecification.getId());
43 | return Observable.just(shoppingList);
44 | }
45 | }
46 | return Observable.error(new IllegalArgumentException("No items match Specification provided."));
47 | }
48 |
49 | @Override
50 | public Single add(ShoppingList item) {
51 | shoppingLists.add(item); // add or update given shopping list
52 | return Single.just(item);
53 | }
54 |
55 | @Override
56 | public Single update(ShoppingList item) {
57 | shoppingLists.add(item); // add or update given shopping list
58 | return Single.just(item);
59 | }
60 |
61 | @Override
62 | public Completable removeItem(Specification specification) {
63 | ItemByIdSpecification byIdSpecification = (ItemByIdSpecification) specification;
64 | for (ShoppingList shoppingList : shoppingLists) {
65 | if (shoppingList.getId().equals(byIdSpecification.getId())) {
66 | shoppingLists.remove(shoppingList);
67 | return Completable.complete();
68 | }
69 | }
70 | return Completable.error(new IllegalArgumentException("No items match Specification provided."));
71 | }
72 |
73 | private void initializeShoppingLists() {
74 | List shoppingList1Items = new ArrayList<>();
75 | shoppingList1Items.add(new ShoppingListItem("bread"));
76 | shoppingList1Items.add(new ShoppingListItem("milk"));
77 | ShoppingList shoppingList1 = new ShoppingList(shoppingList1Items);
78 | shoppingList1.setId("1");
79 | shoppingLists.add(shoppingList1);
80 |
81 | List shoppingList2Items = new ArrayList<>();
82 | shoppingList2Items.add(new ShoppingListItem("cereal"));
83 | shoppingList2Items.add(new ShoppingListItem("cookies"));
84 | ShoppingList shoppingList2 = new ShoppingList(shoppingList2Items);
85 | shoppingList2.setId("2");
86 | shoppingLists.add(shoppingList2);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jshvarts/shoppinglist/lobby/fragments/DeleteCompletedItemsViewModel.java:
--------------------------------------------------------------------------------
1 | package com.jshvarts.shoppinglist.lobby.fragments;
2 |
3 | import android.app.Application;
4 | import android.arch.lifecycle.AndroidViewModel;
5 | import android.arch.lifecycle.LiveData;
6 | import android.arch.lifecycle.MutableLiveData;
7 | import android.content.Context;
8 | import android.hardware.SensorManager;
9 |
10 | import com.jshvarts.shoppinglist.common.domain.model.DatabaseConstants;
11 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingList;
12 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingListDataHelper;
13 | import com.jshvarts.shoppinglist.common.viewmodel.SingleLiveEvent;
14 | import com.jshvarts.shoppinglist.rx.SchedulersFacade;
15 | import com.squareup.seismic.ShakeDetector;
16 |
17 | import io.reactivex.disposables.CompositeDisposable;
18 | import timber.log.Timber;
19 |
20 | public class DeleteCompletedItemsViewModel extends AndroidViewModel implements ShakeDetector.Listener {
21 |
22 | private final LoadShoppingListUseCase loadShoppingListUseCase;
23 |
24 | private final UpdateShoppingListUseCase updateShoppingListUseCase;
25 |
26 | private final SchedulersFacade schedulersFacade;
27 |
28 | private final ShoppingListDataHelper shoppingListDataHelper;
29 |
30 | private final SensorManager sensorManager;
31 |
32 | private final ShakeDetector shakeDetector;
33 |
34 | private final CompositeDisposable disposables = new CompositeDisposable();
35 |
36 | private MutableLiveData completedItemsDeleted = new SingleLiveEvent<>();
37 |
38 | public DeleteCompletedItemsViewModel(Application application,
39 | LoadShoppingListUseCase loadShoppingListUseCase,
40 | UpdateShoppingListUseCase updateShoppingListUseCase,
41 | SchedulersFacade schedulersFacade,
42 | ShoppingListDataHelper shoppingListDataHelper) {
43 | super(application);
44 | this.loadShoppingListUseCase = loadShoppingListUseCase;
45 | this.updateShoppingListUseCase = updateShoppingListUseCase;
46 | this.schedulersFacade = schedulersFacade;
47 | this.shoppingListDataHelper = shoppingListDataHelper;
48 |
49 | sensorManager = (SensorManager) application.getSystemService(Context.SENSOR_SERVICE);
50 | shakeDetector = new ShakeDetector(this);
51 | shakeDetector.start(sensorManager);
52 | }
53 |
54 | @Override
55 | protected void onCleared() {
56 | disposables.clear();
57 | }
58 |
59 | @Override
60 | public void hearShake() {
61 | loadShoppingListAndDeleteCompletedItems();
62 | }
63 |
64 | LiveData getCompletedItemsDeleted() {
65 | return completedItemsDeleted;
66 | }
67 |
68 | private void loadShoppingListAndDeleteCompletedItems() {
69 | disposables.add(loadShoppingListUseCase.loadShoppingList(DatabaseConstants.DEFAULT_SHOPPING_LIST_ID)
70 | .subscribeOn(schedulersFacade.io())
71 | .observeOn(schedulersFacade.ui())
72 | .firstElement()
73 | .subscribe(shoppingList -> deleteCompletedItems(shoppingList),
74 | throwable -> Timber.e(throwable))
75 | );
76 | }
77 |
78 | private void deleteCompletedItems(ShoppingList shoppingList) {
79 | if (!shoppingListDataHelper.removeCompletedItems(shoppingList)) {
80 | Timber.d("shake but no completed items available");
81 | return;
82 | }
83 | Timber.d("deleting completed items after shake");
84 | disposables.add(updateShoppingListUseCase.updateShoppingList(shoppingList)
85 | .subscribeOn(schedulersFacade.io())
86 | .observeOn(schedulersFacade.ui())
87 | .subscribe(updatedShoppingList -> {
88 | completedItemsDeleted.setValue(true);
89 | }, throwable -> Timber.e(throwable)
90 | )
91 | );
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | project.ext {
4 | supportLibraryVersion = "26.0.2"
5 | daggerVersion = "2.12"
6 | butterKnifeVersion = "8.7.0"
7 | rxJavaVersion = "2.1.0"
8 | rxAndroidVersion = "2.0.1"
9 | timberVersion = "4.5.1"
10 | lifecycleVersion = "1.0.0-alpha3"
11 | firebaseVersion="11.0.2"
12 | multiDexVersion = "1.0.1"
13 | guavaVersion = "20.0"
14 | seismicVersion = "1.0.2"
15 | priorityJobQueueVersion = "2.0.1"
16 | junitVersion = "4.12"
17 | mockitoVersion = "2.7.22"
18 | javaHamcrestVersion = "2.0.0.0"
19 | powerMockVersion = "1.7.0RC4"
20 | }
21 |
22 | android {
23 | compileSdkVersion 26
24 | buildToolsVersion '26.0.2'
25 | defaultConfig {
26 | applicationId "com.jshvarts.shoppinglist"
27 | minSdkVersion 20
28 | targetSdkVersion 26
29 | versionCode 1
30 | versionName "1.0"
31 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
32 |
33 | compileOptions {
34 | sourceCompatibility JavaVersion.VERSION_1_8
35 | targetCompatibility JavaVersion.VERSION_1_8
36 | }
37 |
38 | multiDexEnabled true
39 | }
40 | buildTypes {
41 | debug {
42 | //applicationIdSuffix = ".debug"
43 | testCoverageEnabled = true
44 | }
45 |
46 | release {
47 | minifyEnabled false
48 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
49 | }
50 | }
51 | }
52 |
53 | apply from: "$rootDir/config/jacoco.gradle"
54 |
55 | dependencies {
56 | compile fileTree(dir: 'libs', include: ['*.jar'])
57 | compile "com.android.support:appcompat-v7:$project.supportLibraryVersion"
58 |
59 | compile "com.android.support:multidex:$project.multiDexVersion"
60 |
61 | // Dagger core
62 | annotationProcessor "com.google.dagger:dagger-compiler:$project.daggerVersion"
63 | compile "com.google.dagger:dagger:$project.daggerVersion"
64 |
65 | // Dagger Android
66 | annotationProcessor "com.google.dagger:dagger-android-processor:$project.daggerVersion"
67 | compile "com.google.dagger:dagger-android-support:$project.daggerVersion"
68 | // if you are not using support library, include this instead
69 | compile "com.google.dagger:dagger-android:$project.daggerVersion"
70 |
71 | // ButterKnife
72 | compile "com.jakewharton:butterknife:$project.butterKnifeVersion"
73 | annotationProcessor "com.jakewharton:butterknife-compiler:$project.butterKnifeVersion"
74 |
75 | // ReactiveX
76 | compile "io.reactivex.rxjava2:rxjava:$project.rxJavaVersion"
77 | compile "io.reactivex.rxjava2:rxandroid:$project.rxAndroidVersion"
78 |
79 | // Timber
80 | compile "com.jakewharton.timber:timber:$project.timberVersion"
81 |
82 | // Lifecycle
83 | compile "android.arch.lifecycle:runtime:$project.lifecycleVersion"
84 | compile "android.arch.lifecycle:extensions:$project.lifecycleVersion"
85 | annotationProcessor "android.arch.lifecycle:compiler:$project.lifecycleVersion"
86 |
87 | // Firebase
88 | implementation "com.google.firebase:firebase-database:$project.firebaseVersion"
89 |
90 | //Firebase Auth with Google acct
91 | //compile "com.google.firebase:firebase-auth:$project.firebaseVersion"
92 | //compile "com.google.android.gms:play-services-auth:$project.firebaseVersion"
93 |
94 | // Guava
95 | compile "com.google.guava:guava:$project.guavaVersion"
96 |
97 | // Support Design
98 | compile "com.android.support:design:$project.supportLibraryVersion"
99 |
100 | // CardView
101 | compile "com.android.support:cardview-v7:$project.supportLibraryVersion"
102 |
103 | // RecyclerView
104 | compile "com.android.support:recyclerview-v7:$project.supportLibraryVersion"
105 |
106 | // Seismic, device shake detection
107 | compile "com.squareup:seismic:$project.seismicVersion"
108 |
109 | // Priority Job Queue
110 | compile "com.birbit:android-priority-jobqueue:$project.priorityJobQueueVersion"
111 |
112 | // JUnit
113 | testCompile "junit:junit:$project.junitVersion"
114 |
115 | // Mockito
116 | testCompile "org.mockito:mockito-core:$project.mockitoVersion"
117 |
118 | // Hamcrest
119 | testCompile "org.hamcrest:java-hamcrest:$project.javaHamcrestVersion"
120 |
121 | // PowerMock
122 | testCompile "org.powermock:powermock-core:$project.powerMockVersion"
123 | }
124 |
125 | apply plugin: 'com.google.gms.google-services'
126 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/test/java/com/jshvarts/shoppinglist/common/domain/model/ShoppingListDataHelperTest.java:
--------------------------------------------------------------------------------
1 | package com.jshvarts.shoppinglist.common.domain.model;
2 |
3 | import org.junit.Before;
4 | import org.junit.Test;
5 | import org.mockito.Mockito;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | import static org.hamcrest.MatcherAssert.assertThat;
11 | import static org.hamcrest.Matchers.is;
12 | import static org.mockito.Mockito.mock;
13 | import static org.mockito.Mockito.when;
14 |
15 | /**
16 | * Unit tests for {@link ShoppingListDataHelper}.
17 | */
18 | public class ShoppingListDataHelperTest {
19 |
20 | ShoppingListDataHelper testSubject;
21 |
22 | ShoppingList shoppingList;
23 |
24 | @Before
25 | public void setUp() throws Exception {
26 | testSubject = new ShoppingListDataHelper();
27 | shoppingList = new ShoppingList();
28 | }
29 |
30 | @Test
31 | public void completeItem_givenItemIsInRange_marksItemsCompleted() throws Exception {
32 | // GIVEN
33 | final String itemName1 = "item name 1";
34 | final String itemName2 = "item name 2";
35 | ShoppingListItem shoppingListItem1 = new ShoppingListItem(itemName1);
36 | ShoppingListItem shoppingListItem2 = new ShoppingListItem(itemName2);
37 | List items = new ArrayList<>();
38 | items.add(shoppingListItem1);
39 | items.add(shoppingListItem2);
40 | shoppingList.setItems(items);
41 |
42 | // WHEN
43 | boolean result = testSubject.completeItem(shoppingList, 0);
44 |
45 | // THEN
46 | assertThat(shoppingList.getItems().get(0).getCompleted(), is(true));
47 | assertThat(shoppingList.getItems().get(1).getCompleted(), is(false));
48 | assertThat(result, is(true));
49 | }
50 |
51 | @Test
52 | public void completeItem_givenItemIsOutOfRange_doesNotMarkItemsComplete() throws Exception {
53 | // GIVEN
54 | final String itemName1 = "item name 1";
55 | final String itemName2 = "item name 2";
56 | ShoppingListItem shoppingListItem1 = new ShoppingListItem(itemName1);
57 | ShoppingListItem shoppingListItem2 = new ShoppingListItem(itemName2);
58 | List items = new ArrayList<>();
59 | items.add(shoppingListItem1);
60 | items.add(shoppingListItem2);
61 | shoppingList.setItems(items);
62 |
63 | // WHEN
64 | boolean result = testSubject.completeItem(shoppingList, 2);
65 |
66 | // THEN
67 | assertThat(shoppingList.getItems().get(0).getCompleted(), is(false));
68 | assertThat(shoppingList.getItems().get(1).getCompleted(), is(false));
69 | assertThat(result, is(false));
70 | }
71 |
72 | @Test
73 | public void completeItem_givenListIsEmpty_doesNotMarkItemsComplete() throws Exception {
74 | // GIVEN
75 | List items = new ArrayList<>();
76 | shoppingList.setItems(items);
77 |
78 | // WHEN
79 | boolean result = testSubject.completeItem(shoppingList, 0);
80 |
81 | // THEN
82 | assertThat(result, is(false));
83 | }
84 |
85 | @Test
86 | public void addItem_whenItemCountIsZero_appendsItem() throws Exception {
87 | // GIVEN
88 | String itemName = "item name 1";
89 |
90 | // WHEN
91 | testSubject.addItem(shoppingList, itemName);
92 |
93 | // GIVEN
94 | assertThat(shoppingList.getItems().size(), is(1));
95 | assertThat(shoppingList.getItems().get(0).getName(), is(itemName));
96 | }
97 |
98 | @Test
99 | public void addItem_appendsItem() throws Exception {
100 | // GIVEN
101 | final String itemName1 = "item name 1";
102 | final String itemName2 = "item name 2";
103 | ShoppingListItem shoppingListItem1 = new ShoppingListItem(itemName1);
104 | ShoppingListItem shoppingListItem2 = new ShoppingListItem(itemName2);
105 | List items = new ArrayList<>();
106 | items.add(shoppingListItem1);
107 | items.add(shoppingListItem2);
108 | shoppingList.setItems(items);
109 |
110 | String newItemName = "item name 3";
111 |
112 | // WHEN
113 | testSubject.addItem(shoppingList, newItemName);
114 |
115 | // GIVEN
116 | assertThat(shoppingList.getItems().size(), is(3));
117 | assertThat(shoppingList.getItems().get(0).getName(), is(itemName1));
118 | assertThat(shoppingList.getItems().get(1).getName(), is(itemName2));
119 | assertThat(shoppingList.getItems().get(2).getName(), is(newItemName));
120 | }
121 |
122 | @Test
123 | public void removeCompletedItems_whenShoppingListIsEmpty_doesNotRemoveItems() throws Exception {
124 | shoppingList = mock(ShoppingList.class);
125 |
126 | // WHEN
127 | boolean result = testSubject.removeCompletedItems(shoppingList);
128 |
129 | // THEN
130 | assertThat(result, is(false));
131 | }
132 |
133 | @Test
134 | public void removeCompletedItems_whenShoppingListHasCompletedItems_removesCompletedItems() throws Exception {
135 | shoppingList = Mockito.mock(ShoppingList.class);
136 | final String itemName1 = "item name 1";
137 | final String itemName2 = "item name 2";
138 | ShoppingListItem shoppingListItem1 = new ShoppingListItem(itemName1);
139 | shoppingListItem1.setCompleted(true);
140 | ShoppingListItem shoppingListItem2 = new ShoppingListItem(itemName2);
141 | shoppingListItem2.setCompleted(false);
142 | List items = new ArrayList<>();
143 | items.add(shoppingListItem1);
144 | items.add(shoppingListItem2);
145 |
146 | when(shoppingList.getItems()).thenReturn(items);
147 |
148 | // WHEN
149 | boolean result = testSubject.removeCompletedItems(shoppingList);
150 |
151 | // THEN
152 | assertThat(result, is(true));
153 | assertThat(shoppingList.getItems().size(), is(1));
154 | assertThat(shoppingList.getItems().get(0).getName(), is(itemName2));
155 | }
156 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jshvarts/shoppinglist/common/domain/model/firebase/FirebaseShoppingListRepository.java:
--------------------------------------------------------------------------------
1 | package com.jshvarts.shoppinglist.common.domain.model.firebase;
2 |
3 | import com.google.firebase.database.DataSnapshot;
4 | import com.google.firebase.database.DatabaseError;
5 | import com.google.firebase.database.DatabaseReference;
6 | import com.google.firebase.database.FirebaseDatabase;
7 | import com.google.firebase.database.Query;
8 | import com.google.firebase.database.ValueEventListener;
9 | import com.jshvarts.shoppinglist.common.domain.model.ItemsSpecification;
10 | import com.jshvarts.shoppinglist.common.domain.model.Repository;
11 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingList;
12 | import com.jshvarts.shoppinglist.common.domain.model.ItemByIdSpecification;
13 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingListItemNameComparator;
14 | import com.jshvarts.shoppinglist.common.domain.model.Specification;
15 |
16 | import java.util.ArrayList;
17 | import java.util.Collections;
18 | import java.util.List;
19 |
20 | import javax.inject.Singleton;
21 |
22 | import io.reactivex.Completable;
23 | import io.reactivex.Observable;
24 | import io.reactivex.Single;
25 | import timber.log.Timber;
26 |
27 | @Singleton
28 | public class FirebaseShoppingListRepository implements Repository {
29 | private final FirebaseDatabase database;
30 |
31 | public FirebaseShoppingListRepository() {
32 | database = FirebaseDatabase.getInstance();
33 | database.setPersistenceEnabled(true);
34 | }
35 |
36 | @Override
37 | public Single> getItems(Specification specification) {
38 | ItemsSpecification itemsSpecification = (ItemsSpecification) specification;
39 | List shoppingLists = new ArrayList<>();
40 | return Single.create(emitter -> {
41 | ValueEventListener valueEventListener = new ValueEventListener() {
42 | @Override
43 | public void onDataChange(DataSnapshot dataSnapshot) {
44 | if (dataSnapshot.exists()) {
45 | for (DataSnapshot childSnapshot : dataSnapshot.getChildren()) {
46 | ShoppingList shoppingList = childSnapshot.getValue(ShoppingList.class);
47 | shoppingList.setId(childSnapshot.getKey());
48 | shoppingLists.add(shoppingList);
49 | emitter.onSuccess(shoppingLists);
50 | }
51 | } else {
52 | emitter.onError(new IllegalArgumentException("Unable to find any shopping lists"));
53 | }
54 | }
55 |
56 | @Override
57 | public void onCancelled(DatabaseError databaseError) {
58 | Timber.e(databaseError.toException(), "getItems:onCancelled.");
59 | emitter.onError(databaseError.toException());
60 | }
61 | };
62 |
63 | Query shoppingListQuery = database.getReference().orderByKey().limitToLast(itemsSpecification.getMaxCount());
64 | shoppingListQuery.addValueEventListener(valueEventListener);
65 | emitter.setCancellable(() -> shoppingListQuery.removeEventListener(valueEventListener));
66 | });
67 | }
68 |
69 | @Override
70 | public Observable getItem(Specification specification) {
71 | ItemByIdSpecification byIdSpecification = (ItemByIdSpecification) specification;
72 | return Observable.create(emitter -> {
73 | ValueEventListener valueEventListener = new ValueEventListener() {
74 | @Override
75 | public void onDataChange(DataSnapshot dataSnapshot) {
76 | if (dataSnapshot.exists()) {
77 | ShoppingList shoppingList = dataSnapshot.getValue(ShoppingList.class);
78 | shoppingList.setId(dataSnapshot.getKey());
79 | // TODO ideally left Firebase API do the sorting
80 | Collections.sort(shoppingList.getItems(), new ShoppingListItemNameComparator());
81 | emitter.onNext(shoppingList);
82 | }
83 | }
84 |
85 | @Override
86 | public void onCancelled(DatabaseError databaseError) {
87 | Timber.e(databaseError.toException(), "getItem:onCancelled.");
88 | emitter.onError(databaseError.toException());
89 | }
90 | };
91 |
92 | DatabaseReference shoppingListRef = database.getReference().child(byIdSpecification.getId());
93 | shoppingListRef.addValueEventListener(valueEventListener);
94 | emitter.setCancellable(() -> shoppingListRef.removeEventListener(valueEventListener));
95 | });
96 | }
97 |
98 | @Override
99 | public Single add(ShoppingList shoppingList) {
100 | DatabaseReference shoppingListRef = database.getReference().push();
101 | shoppingListRef.setValue(shoppingList, (databaseError, databaseReference) -> {
102 | if (databaseError != null) {
103 | Timber.e(databaseError.toException(), "add:databaseError.");
104 | Single.error(databaseError.toException());
105 | }
106 | });
107 | shoppingList.setId(shoppingListRef.getKey());
108 | return Single.just(shoppingList);
109 | }
110 |
111 | @Override
112 | public Single update(ShoppingList shoppingList) {
113 | DatabaseReference shoppingListRef = database.getReference().child(shoppingList.getId());
114 | shoppingListRef.setValue(shoppingList, (databaseError, databaseReference) -> {
115 | if (databaseError != null) {
116 | Timber.e(databaseError.toException(), "update:databaseError.");
117 | Single.error(databaseError.toException());
118 | }
119 | });
120 | return Single.just(shoppingList);
121 | }
122 |
123 | @Override
124 | public Completable removeItem(Specification specification) {
125 | throw new RuntimeException("Not implemented");
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jshvarts/shoppinglist/lobby/fragments/ViewShoppingListFragment.java:
--------------------------------------------------------------------------------
1 | package com.jshvarts.shoppinglist.lobby.fragments;
2 |
3 | import android.arch.lifecycle.LifecycleFragment;
4 | import android.arch.lifecycle.ViewModelProviders;
5 | import android.content.Context;
6 | import android.os.Bundle;
7 | import android.support.annotation.Nullable;
8 | import android.support.design.widget.FloatingActionButton;
9 | import android.support.v4.app.Fragment;
10 | import android.support.v4.app.FragmentManager;
11 | import android.support.v7.widget.LinearLayoutManager;
12 | import android.support.v7.widget.RecyclerView;
13 | import android.support.v7.widget.helper.ItemTouchHelper;
14 | import android.view.LayoutInflater;
15 | import android.view.View;
16 | import android.view.ViewGroup;
17 | import android.widget.ProgressBar;
18 | import android.widget.Toast;
19 |
20 | import com.jshvarts.shoppinglist.R;
21 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingList;
22 |
23 | import java.util.ArrayList;
24 |
25 | import javax.inject.Inject;
26 |
27 | import butterknife.BindView;
28 | import butterknife.ButterKnife;
29 | import butterknife.Unbinder;
30 | import dagger.android.support.AndroidSupportInjection;
31 |
32 | public class ViewShoppingListFragment extends LifecycleFragment {
33 |
34 | public static final String TAG = ViewShoppingListFragment.class.getSimpleName();
35 |
36 | private static final int FAB_REDISPLAY_DELAY_MILLIS = 300;
37 |
38 | @Inject
39 | DeleteCompletedItemsViewModelFactory deleteCompletedItemsViewModelFactory;
40 |
41 | @Inject
42 | ShoppingListViewModelFactory shoppingListViewModelFactory;
43 |
44 | @BindView(R.id.shopping_list_recycler_view)
45 | RecyclerView recyclerView;
46 |
47 | @BindView(R.id.loading_indicator)
48 | ProgressBar loadingIndicator;
49 |
50 | @BindView(R.id.fab)
51 | FloatingActionButton fab;
52 |
53 | private ShoppingListAdapter recyclerViewAdapter;
54 |
55 | private Unbinder unbinder;
56 |
57 | private DeleteCompletedItemsViewModel deleteCompletedItemsViewModel;
58 |
59 | private ShoppingListViewModel viewModel;
60 |
61 | private FragmentManager.OnBackStackChangedListener backStackChangedListener = () -> {
62 | if (getChildFragmentManager().getBackStackEntryCount() > 0) {
63 | fab.hide();
64 | } else {
65 | showFabWithDelay();
66 | }
67 | };
68 |
69 | private ItemTouchHelper.SimpleCallback simpleCallback
70 | = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
71 |
72 | @Override
73 | public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
74 | return false;
75 | }
76 |
77 | @Override
78 | public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
79 | final int position = viewHolder.getAdapterPosition();
80 | viewModel.completeShoppingListItem(position);
81 | }
82 | };
83 |
84 | @Override
85 | public void onAttach(Context context) {
86 | AndroidSupportInjection.inject(this);
87 | super.onAttach(context);
88 | }
89 |
90 | @Nullable
91 | @Override
92 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
93 | View view = inflater.inflate(R.layout.shopping_list_fragment, container, false);
94 | unbinder = ButterKnife.bind(this, view);
95 |
96 | fab.setOnClickListener(v -> attachAddShoppingListItemFragment());
97 |
98 | initRecyclerView();
99 |
100 | return view;
101 | }
102 |
103 | @Override
104 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
105 | super.onViewCreated(view, savedInstanceState);
106 |
107 | deleteCompletedItemsViewModel = ViewModelProviders.of(getActivity(), deleteCompletedItemsViewModelFactory).get(DeleteCompletedItemsViewModel.class);
108 | deleteCompletedItemsViewModel.getCompletedItemsDeleted().observe(this, isSuccess -> handleCompletedItemsDeletedStatus(isSuccess));
109 |
110 | viewModel = ViewModelProviders.of(getActivity(), shoppingListViewModelFactory).get(ShoppingListViewModel.class);
111 | viewModel.loadShoppingList();
112 |
113 | viewModel.getLoadingIndicatorStatus().observe(this, isActive -> loadingIndicator.setVisibility(isActive ? View.VISIBLE : View.GONE));
114 |
115 | viewModel.getShoppingList().observe(this, shoppingList -> {
116 | displayShoppingList(shoppingList);
117 | fab.show();
118 | });
119 |
120 | getChildFragmentManager().addOnBackStackChangedListener(backStackChangedListener);
121 | }
122 |
123 | @Override
124 | public void onDestroyView() {
125 | super.onDestroyView();
126 | unbinder.unbind();
127 | }
128 |
129 | private void handleCompletedItemsDeletedStatus(boolean isSuccess) {
130 | if (isSuccess) {
131 | Toast.makeText(getActivity(), R.string.deleted_completed_items_success_text, Toast.LENGTH_SHORT).show();
132 | }
133 | }
134 |
135 | private void displayShoppingList(ShoppingList shoppingList) {
136 | // investigate clearing and invalidating adapter instead
137 | recyclerViewAdapter = new ShoppingListAdapter(shoppingList.getItems());
138 | recyclerView.setAdapter(recyclerViewAdapter);
139 | recyclerView.getAdapter().notifyDataSetChanged();
140 | }
141 |
142 | private void attachAddShoppingListItemFragment() {
143 | fab.hide();
144 | Fragment addShoppingListItemFragment = new AddShoppingListItemFragment();
145 | getChildFragmentManager().beginTransaction()
146 | .addToBackStack(AddShoppingListItemFragment.TAG)
147 | .add(R.id.shopping_list_fragment_root_view, addShoppingListItemFragment, AddShoppingListItemFragment.TAG)
148 | .commit();
149 | }
150 |
151 | private void initRecyclerView() {
152 | recyclerView.setHasFixedSize(true);
153 |
154 | RecyclerView.LayoutManager recyclerViewLayoutManager = new LinearLayoutManager(getActivity());
155 | recyclerView.setLayoutManager(recyclerViewLayoutManager);
156 |
157 | recyclerViewAdapter = new ShoppingListAdapter(new ArrayList<>());
158 | recyclerView.setAdapter(recyclerViewAdapter);
159 |
160 | new ItemTouchHelper(simpleCallback).attachToRecyclerView(recyclerView);
161 | }
162 |
163 | private void showFabWithDelay() {
164 | fab.getHandler().postDelayed(() -> fab.show(), FAB_REDISPLAY_DELAY_MILLIS);
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/app/src/test/java/com/jshvarts/shoppinglist/lobby/fragments/ShoppingListViewModelTest.java:
--------------------------------------------------------------------------------
1 | package com.jshvarts.shoppinglist.lobby.fragments;
2 |
3 | import android.arch.lifecycle.LiveData;
4 | import android.arch.lifecycle.MutableLiveData;
5 |
6 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingList;
7 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingListDataHelper;
8 | import com.jshvarts.shoppinglist.common.domain.model.ShoppingListItem;
9 | import com.jshvarts.shoppinglist.rx.SchedulersFacade;
10 | import com.jshvarts.shoppinglist.rx.SchedulersFacadeUtils;
11 |
12 | import org.junit.Before;
13 | import org.junit.Test;
14 | import org.mockito.ArgumentMatchers;
15 | import org.mockito.InjectMocks;
16 | import org.mockito.Mock;
17 | import org.mockito.MockitoAnnotations;
18 | import org.powermock.reflect.Whitebox;
19 |
20 | import java.util.ArrayList;
21 | import java.util.List;
22 |
23 | import io.reactivex.Single;
24 | import io.reactivex.disposables.CompositeDisposable;
25 | import io.reactivex.schedulers.TestScheduler;
26 |
27 | import static org.hamcrest.MatcherAssert.assertThat;
28 | import static org.hamcrest.Matchers.is;
29 | import static org.mockito.Mockito.mock;
30 | import static org.mockito.Mockito.never;
31 | import static org.mockito.Mockito.verify;
32 | import static org.mockito.Mockito.when;
33 | import static org.mockito.Mockito.verifyZeroInteractions;
34 |
35 | /**
36 | * Unit tests for {@link ShoppingListViewModel}.
37 | */
38 | public class ShoppingListViewModelTest {
39 |
40 | @InjectMocks
41 | private ShoppingListViewModel testSubject;
42 |
43 | @Mock
44 | private LoadShoppingListUseCase loadShoppingListUseCase;
45 |
46 | @Mock
47 | private UpdateShoppingListUseCase updateShoppingListUseCase;
48 |
49 | @Mock
50 | private SchedulersFacade schedulersFacade;
51 |
52 | @Mock
53 | private ShoppingListDataHelper shoppingListDataHelper;
54 |
55 | @Mock
56 | private CompositeDisposable disposables;
57 |
58 | private TestScheduler testScheduler;
59 |
60 | @Before
61 | public void setUp() throws Exception {
62 | MockitoAnnotations.initMocks(this);
63 |
64 | testScheduler = SchedulersFacadeUtils.setupSchedulersFacade(schedulersFacade);
65 |
66 | Whitebox.setInternalState(testSubject, "disposables", disposables);
67 | }
68 |
69 | @Test
70 | public void onCleared_clearsDisposables() throws Exception {
71 | // WHEN
72 | testSubject.onCleared();
73 |
74 | // THEN
75 | verify(disposables).clear();
76 | }
77 |
78 | @Test
79 | public void getShoppingList_returnsLiveShoppingList() throws Exception {
80 | // GIVEN
81 | LiveData expected = mock(MutableLiveData.class);
82 | Whitebox.setInternalState(testSubject, "liveShoppingList", expected);
83 |
84 | // WHEN
85 | LiveData result = testSubject.getShoppingList();
86 |
87 | // THEN
88 | assertThat(result, is(expected));
89 | }
90 |
91 | @Test
92 | public void getLoadingIndicatorStatus_returnsLoadingIndicatorStatus() throws Exception {
93 | // GIVEN
94 | LiveData expected = mock(MutableLiveData.class);
95 | Whitebox.setInternalState(testSubject, "loadingIndicatorStatus", expected);
96 |
97 | // WHEN
98 | LiveData result = testSubject.getLoadingIndicatorStatus();
99 |
100 | // THEN
101 | assertThat(result, is(expected));
102 | }
103 |
104 | @Test
105 | public void loadShoppingList() throws Exception {
106 | // TODO
107 | }
108 |
109 | @Test
110 | public void completeShoppingListItem_whenItemAlreadyCompleted_returnsShoppingList() throws Exception {
111 | // GIVEN
112 | int itemIndex = 0;
113 | ShoppingList shoppingList = given_shoppingListWithOneItem(true);
114 | MutableLiveData liveShoppingList = given_liveShoppingList(shoppingList);
115 |
116 | // WHEN
117 | testSubject.completeShoppingListItem(itemIndex);
118 |
119 | // THEN
120 | verify(liveShoppingList).setValue(shoppingList);
121 | assertThat(liveShoppingList.getValue(), is(shoppingList));
122 | verifyZeroInteractions(shoppingListDataHelper);
123 | verifyZeroInteractions(updateShoppingListUseCase);
124 | }
125 |
126 | @Test
127 | public void completeShoppingListItem_whenItemNotCompletedAndRepositoryEmitsSuccess_completesItem() throws Exception {
128 | // GIVEN
129 | int itemIndex = 0;
130 | ShoppingList shoppingList = given_shoppingListWithOneItem(false);
131 | MutableLiveData liveShoppingList = given_liveShoppingList(shoppingList);
132 | when(updateShoppingListUseCase.updateShoppingList(shoppingList)).thenReturn(Single.just(shoppingList));
133 |
134 | // WHEN
135 | testSubject.completeShoppingListItem(itemIndex);
136 | testScheduler.triggerActions();
137 |
138 | // THEN
139 | verify(shoppingListDataHelper).completeItem(shoppingList, itemIndex);
140 | verify(liveShoppingList).setValue(shoppingList);
141 | }
142 |
143 |
144 | @Test
145 | public void completeShoppingListItem_whenItemNotCompletedAndRepositoryEmitsError_completesItem() throws Exception {
146 | // GIVEN
147 | int itemIndex = 0;
148 | ShoppingList shoppingList = given_shoppingListWithOneItem(false);
149 | MutableLiveData liveShoppingList = given_liveShoppingList(shoppingList);
150 | when(updateShoppingListUseCase.updateShoppingList(shoppingList)).thenReturn(Single.error(new Exception()));
151 |
152 | // WHEN
153 | testSubject.completeShoppingListItem(itemIndex);
154 | testScheduler.triggerActions();
155 |
156 | // THEN
157 | verify(shoppingListDataHelper).completeItem(shoppingList, itemIndex);
158 | verify(liveShoppingList, never()).setValue(ArgumentMatchers.any(ShoppingList.class));
159 | }
160 |
161 | private ShoppingList given_shoppingListWithOneItem(boolean itemCompleted) {
162 | ShoppingList shoppingList = new ShoppingList();
163 | List items = new ArrayList<>();
164 | ShoppingListItem item = new ShoppingListItem();
165 | item.setCompleted(itemCompleted);
166 | items.add(item);
167 | shoppingList.setItems(items);
168 | return shoppingList;
169 | }
170 |
171 | private MutableLiveData given_liveShoppingList(ShoppingList shoppingList) {
172 | MutableLiveData liveShoppingList = mock(MutableLiveData.class);
173 | when(liveShoppingList.getValue()).thenReturn(shoppingList);
174 | Whitebox.setInternalState(testSubject, "liveShoppingList", liveShoppingList);
175 | return liveShoppingList;
176 | }
177 | }
--------------------------------------------------------------------------------