Code review - prosta aplikacja android

0

Hej,

jestem juniorem, który pracuje na co dzień w Java web. Jednak w wolnym czasie programuję na Androida.

Mam 2 apki na google play, których kod jest kiepski (to były projekty przy których się właściwie uczyłem programować)
Dlatego wziąłem się za naukę pisania czytelnego, łatwego do rozbudowy kodu.

W tym celu napisałem prosty programik wyszukujący użytkowników na githubie.

Starałem się pisać dobrej jakości kod używając wzorca MVP i wykorzystać popularne ostatnio narzędzia, np. Dagger2, RX Java.

Będę wdzięczny za każdą recenzję i wskazówkę co mogę poprawić.

Link do projektu:
https://github.com/rafalols/GithubSearcher

1
  1. Zostaw decyzje na jakim wątki/threadpooli chce się subskrybować użytkownikowi API

    public Observable<GithubUser> getUsers(List<String> users) {
        return Observable.from(users)
                .flatMap(user -> service.getUser(user))
                .subscribeOn(Schedulers.newThread()); //wywal to
    }

I jeżeli już używasz threadpool, to weź sobie zdefiniuj jedną i nią potem steruj - łatwiej, wygodniej, ładniej.

    private long getCurrentDate() {
        Date date = new Date();
        return date.getTime();
    }

srlsy :) http://geekmonkey.org/2012/09/a-new-date-and-time-api-for-jdk-8/

EDIT:: zapomniałem że Wy nie macie Javy 8 :D ale joda time tez jest
3.

   @Override
    public void putSearchToHistory(String searchString, int numberOfResults) {
        SearchHistory search = new SearchHistory();
        search.setSearchString(searchString);
        search.setSearchDate(getCurrentDate());
        search.setNumberOfResults(numberOfResults);

        realm.copyToRealmOrUpdateInTransaction(search); // nie mozna tak ?

    }
            gitHubApi.searchForUsers(searchString)
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new Subscriber<GithubUsersSearch>() {
                        @Override
                        public void onCompleted() {

                        }

                        @Override
                        public void onError(Throwable e) {
                            toast(activity.getString(R.string.api_error));
                            Log.e(TAG, "onError: ", e);
                        }

                        @Override
                        public void onNext(GithubUsersSearch githubUsersSearch) {
                            cashedUserSearch = githubUsersSearch;
                            onGetUsersList(githubUsersSearch);
                        }
                    });

użyj doOnNext

            gitHubApi.searchForUsers(searchString)
                    .observeOn(AndroidSchedulers.mainThread())
                    .doOnNext(onGetUsersList(githubUsersSearch))
                    .doOnError(oast(activity.getString(R.string.api_error)))
                    .subscribe();
0
        Futures.addCallback(appProductionComponent.gitHubApi(),
                new FutureCallback<GitHubApi>() {
                    @Override
                    public void onSuccess(GitHubApi result) {
                        GitHubSearcherApplication.this.api = result;
                    }

                    @Override
                    public void onFailure(Throwable t) {

                    }
                });

Zrób re-throw, nie wiem, zaloguj(chociaż nie wiem czy w androidzie ma to sens, może wyślij gdzieś gdzie możesz zbierać logi - jakiś agregator po RESTcie pewnie jest dostępny)

       view.setProgressIndicator(true);
        if (savedInstanceState != null){
            user = Parcels.unwrap(savedInstanceState.getParcelable(ARG_USERNAME));
            onGetUser(user);
        }else if (activity.getIntent().hasExtra(ARG_USERNAME)){
            String name = activity.getIntent().getStringExtra(ARG_USERNAME);
            getUserByLogin(name);
        }

nie wiem jaka jest konwencja na pisanie takich handlerów, ale jak to czytam jako noob który nie wie o co chodzi, to nie wiem o co chodzi :)
a jakbym widział np:

       view.setProgressIndicator(true);
        if (savedInstanceState != null){
            zrobCostam();
        }else if (activity.getIntent().hasExtra(ARG_USERNAME)){
            pupabladaCostam();
        }

to bym nie wiedział trochę mniej niż nie wiem teraz :)

        SearchHistory search = new SearchHistory();
        search.setSearchString(searchString);
        search.setSearchDate(getCurrentDate());
        search.setNumberOfResults(numberOfResults);

Ja bym tu builder jakiś zrobił, albo przynajmniej coś takiego

 SearchHistory searchHistory = SearchHistory.of(searchString, getCurrentDate(), numberOfResults);

Dodatkowo, ma to taką zalete że możesz sobie wszystkie pola oznaczyć jako final - co daje gwarancje widzialności w JMM, i możesz bezpiecznie zrobić

 SearchHistory searchHistory = SearchHistory.of(searchString, getCurrentDate(), numberOfResults);
 service.toZwracaObservable
     .flatMap(costam -> service.pupa(costam,searchHistory))
 ...

Jak Twój obiekt będzie miał nie finalne pola, to nie masz gwarancji że ta flatMapa nie wywoła się w innym wątku z jakieś puli wątków i obiekt searchHistory który został przekazany będzie w pełni widzialni (bo nic nie wywołało store czy read barrier )

P.S tutaj, jakby ktoś chciał się z tym kłócić (tak jak ostatni jakiś wojownik z żółtym avatarem) to odsyłam http://stackoverflow.com/questions/8019695/what-is-object-publishing-and-why-do-we-need-it

    @Override
    public Observable<SearchHistory> getHistory() {
        return realm.where(SearchHistory.class).findAllSorted(ORDERBY_FIELD, Sort.DESCENDING)
                .asObservable()
                .flatMap(histories -> Observable.from(histories))
                .take(HISTORY_SIZE);
    }

Ja bym uważał z tymi flatMapami - pamiętaj że coś o wewnętrznie subskrybuje się do obiektu zwróconego z flatMapy (czyli tutaj Observable.from(histories)) - może wykonać się w innym wątku :) Tutaj jest ok, ale warto o tym wiedzieć. Dodatkowo flatMapa nie gwarantuje kolejności :)

Żeby powiedzieć coś więcej, napisz więcej logiki, po-uderzaj więcej do jakichś RESTowych endpointów - opakuj te calle może w Hystrixa (https://github.com/Netflix/Hystrix) ?

1

AD5. Faktycznie... zapomniałem obsłużyć. Mogę logować. Są serwisy zbierające crash logi (pewnie wszystkie error logi), w aplikacji która pójdzie do sklepu Google zadbam o zbieranie logów.

AD6. Teraz pewnie trochę czytelniej. Chociaż dla android devów to dobrze znany kod związany z cyklem życia activity.

if (savedInstanceState != null){
            getUserFromSavedState(savedInstanceState);
        }else if (activity.getIntent().hasExtra(ARG_USERLOGIN)){
            getUserloginFromIntentAndGetUserFromApi();
        }

AD7. Zrobiłem dodatkowy konstruktor z 3 argumentami. Jednak pola nie mogą być finalne wymóg Realma, którego używam, on tworzy obiekty przy użyciu pustego konstruktora i seterów.

AD8. Zmienione na concatMap

Ogólnie dzięki wielkie za code review :)

1

@rafal1606
Z moich uwag:

  1. Wywal BaseAdapter i ListView. Wstaw RecyclerView.
  2. Wywal wszystkie fragmenty i zrób to na Activity. Nie widzę abyś korzystał z navigation drawera, więc fragmenty w tym przypadku wprowadzają dodatkowy, niepotrzebny element abstrakcji. Poza tym jest kilka ciekawych artykułów o tym dlaczego nie należy używać fragmentów.
  3. Jak chcesz z observabla robić update widoku, to nie korzystaj z @MainThread tylko weź to zrób przez RxJava (kod niżej).
  4. Wbij jeszcze retrolambdę do projektu i będzie cacy (kod niżej, ten sam co do update widoku).
  5. Medoty onCreate w presenterach są zbędne. Do takich rzeczy są metody onSaveInstance i onRestoreInstance

Kod do punktów 3 i 4

 .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread()) //pozwoli Ci na updatowanie widoku, który może być tylko aktualizowany z MainThred
                    .subscribe(item ->Log.d("TEST", "onNext"),
                              e -> Log.d("TEST", "onError"),
                              () -> Log.d("TEST", "onCompleted"));

P.S Dodaj jeszcze testy i kotlina, a fajny kod do portfolio będzie :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1