diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 915add9..2490f07 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,8 @@ - + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByDictionary.java b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByDictionary.java new file mode 100644 index 0000000..cf45a2f --- /dev/null +++ b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByDictionary.java @@ -0,0 +1,22 @@ +package com.example.yeseul.movieapp.data.model; + +import android.os.Build; + +import com.example.yeseul.movieapp.pojo.Movie; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class MovieSortByDictionary implements MovieSortStrategy { + @Override + public List sort(List list) { + if(Build.VERSION_CODES.N <= Build.VERSION.SDK_INT) { + list = list.stream().sorted(Comparator.comparing(Movie::getTitle)).collect(Collectors.toList()); + } else { + Collections.sort(list, (o1, o2) -> (o1.getTitle().compareTo(o2.getTitle()))); + } + return list; + } +} diff --git a/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByHighestRating.java b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByHighestRating.java new file mode 100644 index 0000000..3a7295c --- /dev/null +++ b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByHighestRating.java @@ -0,0 +1,24 @@ +package com.example.yeseul.movieapp.data.model; + +import android.os.Build; + +import com.example.yeseul.movieapp.pojo.Movie; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class MovieSortByHighestRating implements MovieSortStrategy { + @Override + public List sort(List list) { + if(Build.VERSION_CODES.N <= Build.VERSION.SDK_INT) { + list = list.stream(). + sorted((o1, o2) -> Double.compare(Double.parseDouble(o2.getUserRating()), Double.parseDouble(o1.getUserRating()))). + collect(Collectors.toList()); + } else { + Collections.sort(list, (o1, o2) -> (Double.compare(Double.parseDouble(o2.getUserRating()), Double.parseDouble(o1.getUserRating())))); + } + return list; + } +} diff --git a/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByLatestPubDate.java b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByLatestPubDate.java new file mode 100644 index 0000000..1b19af8 --- /dev/null +++ b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByLatestPubDate.java @@ -0,0 +1,22 @@ +package com.example.yeseul.movieapp.data.model; + +import android.os.Build; + +import com.example.yeseul.movieapp.pojo.Movie; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class MovieSortByLatestPubDate implements MovieSortStrategy { + @Override + public List sort(List list) { + if(Build.VERSION_CODES.N <= Build.VERSION.SDK_INT) { + list = list.stream().sorted(Comparator.comparing(Movie::getPubDate).reversed()).collect(Collectors.toList()); + } else { + Collections.sort(list, (o1, o2) -> (o2.getPubDate().compareTo(o1.getPubDate()))); + } + return list; + } +} diff --git a/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByLowestRating.java b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByLowestRating.java new file mode 100644 index 0000000..711cc6d --- /dev/null +++ b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByLowestRating.java @@ -0,0 +1,24 @@ +package com.example.yeseul.movieapp.data.model; + +import android.os.Build; + +import com.example.yeseul.movieapp.pojo.Movie; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class MovieSortByLowestRating implements MovieSortStrategy { + @Override + public List sort(List list) { + if(Build.VERSION_CODES.N <= Build.VERSION.SDK_INT) { + list = list.stream(). + sorted((o1, o2) -> Double.compare(Double.parseDouble(o1.getUserRating()), Double.parseDouble(o2.getUserRating()))). + collect(Collectors.toList()); + } else { + Collections.sort(list, (o1, o2) -> (Double.compare(Double.parseDouble(o1.getUserRating()), Double.parseDouble(o2.getUserRating())))); + } + return list; + } +} diff --git a/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByOldPubDate.java b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByOldPubDate.java new file mode 100644 index 0000000..dce80cb --- /dev/null +++ b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortByOldPubDate.java @@ -0,0 +1,24 @@ +package com.example.yeseul.movieapp.data.model; + +import android.annotation.TargetApi; +import android.os.Build; + +import com.example.yeseul.movieapp.pojo.Movie; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class MovieSortByOldPubDate implements MovieSortStrategy { + @Override + public List sort(List list) { + if(Build.VERSION_CODES.N <= Build.VERSION.SDK_INT) { + list = list.stream().sorted(Comparator.comparing(Movie::getPubDate)).collect(Collectors.toList()); + } else { + Collections.sort(list, (o1, o2) -> (o2.getPubDate().compareTo(o1.getPubDate()))); + } + return list; + } +} diff --git a/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortStrategy.java b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortStrategy.java new file mode 100644 index 0000000..788c661 --- /dev/null +++ b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieSortStrategy.java @@ -0,0 +1,13 @@ +package com.example.yeseul.movieapp.data.model; + +import android.annotation.TargetApi; +import android.os.Build; + +import com.example.yeseul.movieapp.pojo.Movie; + +import java.util.List; + +public interface MovieSortStrategy { + + List sort(List list); +} diff --git a/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieStrategyFactory.java b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieStrategyFactory.java new file mode 100644 index 0000000..d4de68d --- /dev/null +++ b/app/src/main/java/com/example/yeseul/movieapp/data/model/MovieStrategyFactory.java @@ -0,0 +1,41 @@ +package com.example.yeseul.movieapp.data.model; + +import android.support.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import static com.example.yeseul.movieapp.data.model.MovieSortStrategy.*; + +public class MovieStrategyFactory { + + // Interface 에 있던 상수들을 Factory Class 로 이동 + // 넘어오는 매개변수가 정확한 타입인지 인증하기 위해 IntDef 사용 + @Retention(RetentionPolicy.SOURCE) + @IntDef({DICTIONARY_ORDER, LATEST_ORDER, OLDEST_ORDER, HIGHEST_RATING_ORDER, LOWEST_RATING_ORDER}) + + public @interface SortTypeDef {} + + public static final int DICTIONARY_ORDER = 0; + public static final int LATEST_ORDER = 1; + public static final int OLDEST_ORDER = 2; + public static final int HIGHEST_RATING_ORDER = 3; + public static final int LOWEST_RATING_ORDER = 4; + + public static MovieSortStrategy create(int idx) { + + switch (idx) { + case DICTIONARY_ORDER: + return new MovieSortByDictionary(); + case LATEST_ORDER: + return new MovieSortByLatestPubDate(); + case OLDEST_ORDER: + return new MovieSortByOldPubDate(); + case HIGHEST_RATING_ORDER: + return new MovieSortByHighestRating(); + case LOWEST_RATING_ORDER: + return new MovieSortByLowestRating(); + } + return null; + } +} diff --git a/app/src/main/java/com/example/yeseul/movieapp/data/remote/RetrofitClient.java b/app/src/main/java/com/example/yeseul/movieapp/data/remote/RetrofitClient.java index 73b3401..a360831 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/data/remote/RetrofitClient.java +++ b/app/src/main/java/com/example/yeseul/movieapp/data/remote/RetrofitClient.java @@ -15,7 +15,6 @@ public class RetrofitClient { private static final String BASE_URL = "https://openapi.naver.com/v1/"; - private static final String CLIENT_ID = "uMJDm4Jz6qzLyQPT5KK9"; private static final String CLIENT_SECRET = "DxWswCSZ20"; diff --git a/app/src/main/java/com/example/yeseul/movieapp/data/source/movie/MovieDataSource.java b/app/src/main/java/com/example/yeseul/movieapp/data/source/movie/MovieDataSource.java index c396ff7..01366ce 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/data/source/movie/MovieDataSource.java +++ b/app/src/main/java/com/example/yeseul/movieapp/data/source/movie/MovieDataSource.java @@ -8,7 +8,10 @@ import io.reactivex.Single; +/* + * Repository 에서 사용할 기능 선언 */ public interface MovieDataSource { + /* API 가 유일한 데이터를 반환하는 경우 Single 이 유용하다.(한 번만 데이터를 발행) */ Single searchMovies(Map request); } diff --git a/app/src/main/java/com/example/yeseul/movieapp/data/source/movie/MovieRemoteDataSource.java b/app/src/main/java/com/example/yeseul/movieapp/data/source/movie/MovieRemoteDataSource.java index eb99220..b07eb07 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/data/source/movie/MovieRemoteDataSource.java +++ b/app/src/main/java/com/example/yeseul/movieapp/data/source/movie/MovieRemoteDataSource.java @@ -11,6 +11,9 @@ import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; +/* + * Remote Data (인터넷에서 가져오는 데이터들) + * Singleton Pattern 으로 작성 -> presenter 에 등록할 때 getInstance 사용 */ public class MovieRemoteDataSource implements MovieDataSource{ private static MovieRemoteDataSource movieRemoteDataSource = null; diff --git a/app/src/main/java/com/example/yeseul/movieapp/data/source/movie/MovieRepository.java b/app/src/main/java/com/example/yeseul/movieapp/data/source/movie/MovieRepository.java index aad2a7c..116faa8 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/data/source/movie/MovieRepository.java +++ b/app/src/main/java/com/example/yeseul/movieapp/data/source/movie/MovieRepository.java @@ -8,6 +8,8 @@ import io.reactivex.Single; +/* + * Presenter 에서 Repository 에 접근할 때 사용하는 ref */ public class MovieRepository implements MovieDataSource { private static MovieRepository movieRepository = null; diff --git a/app/src/main/java/com/example/yeseul/movieapp/mapper/MovieMapper.java b/app/src/main/java/com/example/yeseul/movieapp/mapper/MovieMapper.java index c8cd8fe..3fa4e20 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/mapper/MovieMapper.java +++ b/app/src/main/java/com/example/yeseul/movieapp/mapper/MovieMapper.java @@ -3,12 +3,17 @@ import java.util.HashMap; import java.util.Map; +/* + * */ public class MovieMapper { - public static final String QUERY_STRING_SEARCH_KEY = "query"; - public static final String QUERY_STRING_PAGE_UNIT = "display"; - public static final String QUERY_STRING_START = "start"; + /* private 로 접근 제한자 수정 */ + private static final String QUERY_STRING_SEARCH_KEY = "query"; + private static final String QUERY_STRING_PAGE_UNIT = "display"; + private static final String QUERY_STRING_START = "start"; + /* + * Retrofit 을 이용해 보낼 Query 생성 */ public static Map toRequest(String searchKey, int pageUnit, int start){ Map queryMap = new HashMap<>(); diff --git a/app/src/main/java/com/example/yeseul/movieapp/pojo/Movie.java b/app/src/main/java/com/example/yeseul/movieapp/pojo/Movie.java index 003167c..5c4b54d 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/pojo/Movie.java +++ b/app/src/main/java/com/example/yeseul/movieapp/pojo/Movie.java @@ -5,6 +5,12 @@ public class Movie { + /* + * SerializedName : Gson 을 이용해 Json 에서 Object 로 객체를 변경시킬 때 Json 에 등록된 이름과 연결지어 준다. + * Expose : Expose Annotation 이 존재하면 Gson 을 이용할 때 해당 변수는 노출시키지 않게 됨 + * + * */ + @SerializedName("title") @Expose private String title; diff --git a/app/src/main/java/com/example/yeseul/movieapp/utils/BindingUtil.java b/app/src/main/java/com/example/yeseul/movieapp/utils/BindingUtil.java index 6d75fd8..7731a9f 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/utils/BindingUtil.java +++ b/app/src/main/java/com/example/yeseul/movieapp/utils/BindingUtil.java @@ -20,6 +20,8 @@ public static int setVisibility(boolean isVisible){ return isVisible ? View.VISIBLE : View.GONE; } + /* + * holder_movie_item.xml 에서 rating 과 결합. item 이 변경되면 그에 맞게 data change*/ @BindingAdapter({"rating"}) public static void setRating(RatingBar ratingBar, String userRating){ @@ -29,6 +31,8 @@ public static void setRating(RatingBar ratingBar, String userRating){ ratingBar.setRating((float) rating / 2); } + /* + * holder_movie_item.xml 의 HTML Text 들과 결합. */ @BindingAdapter({"boldText"}) public static void setBoldText(TextView textView, String text){ diff --git a/app/src/main/java/com/example/yeseul/movieapp/utils/GlideUtil.java b/app/src/main/java/com/example/yeseul/movieapp/utils/GlideUtil.java index e45fd90..043f535 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/utils/GlideUtil.java +++ b/app/src/main/java/com/example/yeseul/movieapp/utils/GlideUtil.java @@ -15,6 +15,7 @@ public static void loadImage(ImageView imageView, String url){ GlideApp.with(imageView).load(url).into(imageView); } + /* URL 이 변경되면 해당 Image 를 불러오도록 설정 */ @BindingAdapter({"imageCenterCrop"}) public static void loadImageCenterCrop(ImageView imageView, String url){ diff --git a/app/src/main/java/com/example/yeseul/movieapp/utils/KeyboardUtil.java b/app/src/main/java/com/example/yeseul/movieapp/utils/KeyboardUtil.java index 25a7492..09fdde1 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/utils/KeyboardUtil.java +++ b/app/src/main/java/com/example/yeseul/movieapp/utils/KeyboardUtil.java @@ -4,6 +4,9 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; + +/* + * Keyboard Open, Close Command Handle */ public class KeyboardUtil { public static void closeKeyboard(Context context, EditText editText){ diff --git a/app/src/main/java/com/example/yeseul/movieapp/utils/NetworkStatusCheckUtil.java b/app/src/main/java/com/example/yeseul/movieapp/utils/NetworkStatusCheckUtil.java new file mode 100644 index 0000000..fa8e5f6 --- /dev/null +++ b/app/src/main/java/com/example/yeseul/movieapp/utils/NetworkStatusCheckUtil.java @@ -0,0 +1,20 @@ +package com.example.yeseul.movieapp.utils; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +public class NetworkStatusCheckUtil { + + public static final String NETWORK_STATUS_CHANGE_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; + + public static boolean checkCurrentNetworkStatus(final Context context) { + final ConnectivityManager connectivityManager = + (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + + final NetworkInfo currentNetworkStatus = connectivityManager.getActiveNetworkInfo(); + + // 연결 상태 반환 + return currentNetworkStatus != null && currentNetworkStatus.isConnected(); + } +} diff --git a/app/src/main/java/com/example/yeseul/movieapp/view/BaseActivity.java b/app/src/main/java/com/example/yeseul/movieapp/view/BaseActivity.java index 31e044b..9c9dfa8 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/view/BaseActivity.java +++ b/app/src/main/java/com/example/yeseul/movieapp/view/BaseActivity.java @@ -9,7 +9,14 @@ import android.support.v7.app.AppCompatActivity; import android.widget.Toast; +/* +* Activity 들이 가지는 공통의 설정 +* DataBinding 을 이용한 View-Presenter 사이의 의존성을 약화 +* MVP 패턴을 사용하므로 Presenter 도 동일한 설정 필요 + */ public abstract class BaseActivity extends AppCompatActivity { + + // Log 확인을 위한 TAG 설정. 기본적으로 클래스 이름으로 구분 protected final String TAG = getClass().getSimpleName(); protected B binding; @@ -22,7 +29,9 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { // 화면 세로 고정 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + // View 와 바인딩하기 위한 객체 binding = DataBindingUtil.setContentView(this, getLayoutId()); + // Presenter 를 이용한 이벤트 위임 presenter = getPresenter(); } diff --git a/app/src/main/java/com/example/yeseul/movieapp/view/BasePresenter.java b/app/src/main/java/com/example/yeseul/movieapp/view/BasePresenter.java index a7ac772..9261559 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/view/BasePresenter.java +++ b/app/src/main/java/com/example/yeseul/movieapp/view/BasePresenter.java @@ -1,8 +1,12 @@ package com.example.yeseul.movieapp.view; +/* + * 모든 Presenter 가 공통으로 가져야하는 함수들 선언 */ public interface BasePresenter { - void onViewCreated(); + void onViewAttached(); + + void onViewDetached(); void loadItems(boolean isRefresh); } diff --git a/app/src/main/java/com/example/yeseul/movieapp/view/BaseView.java b/app/src/main/java/com/example/yeseul/movieapp/view/BaseView.java index f6c6504..2d33871 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/view/BaseView.java +++ b/app/src/main/java/com/example/yeseul/movieapp/view/BaseView.java @@ -1,5 +1,7 @@ package com.example.yeseul.movieapp.view; +/* + * 모든 View 가 공통적으로 가져야하는 함수들 선언 */ public interface BaseView { void makeToast(String text); diff --git a/app/src/main/java/com/example/yeseul/movieapp/view/adapter/AdapterContract.java b/app/src/main/java/com/example/yeseul/movieapp/view/adapter/AdapterContract.java index b06cd18..8784db3 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/view/adapter/AdapterContract.java +++ b/app/src/main/java/com/example/yeseul/movieapp/view/adapter/AdapterContract.java @@ -2,13 +2,17 @@ import java.util.List; +/* + * Google Architecture 에서 추천한 Adapter Interface 구조 */ public interface AdapterContract { + /* Adapter 가 달린 View 에서 발생하는 이벤트들만 가짐 */ interface View { void setOnItemClickListener(OnItemClickListener itemClickListener); } + /* Adapter 가 가진 Data 를 조작하는 함수들만 가짐 */ interface Model { T getItem(int position); diff --git a/app/src/main/java/com/example/yeseul/movieapp/view/adapter/BaseRecyclerAdapter.java b/app/src/main/java/com/example/yeseul/movieapp/view/adapter/BaseRecyclerAdapter.java index b715222..d8abd81 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/view/adapter/BaseRecyclerAdapter.java +++ b/app/src/main/java/com/example/yeseul/movieapp/view/adapter/BaseRecyclerAdapter.java @@ -7,7 +7,7 @@ import java.util.ArrayList; import java.util.List; -public abstract class BaseRecyclerAdapter extends RecyclerView.Adapter implements AdapterContract.View, AdapterContract.Model{ +public abstract class BaseRecyclerAdapter extends RecyclerView.Adapter implements AdapterContract.View, AdapterContract.Model{ protected List itemList; protected OnItemClickListener onItemClickListener; @@ -31,6 +31,7 @@ public void setOnItemClickListener(OnItemClickListener onItemClickListener) { this.onItemClickListener = onItemClickListener; } + @Override public long getItemId(int position) { return position; @@ -196,5 +197,6 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int onBindView((H) holder, position); } + // 각 ViewHolder 에 맞게 Item 을 설정해주는 역할 protected abstract void onBindView(H holder, int position); } diff --git a/app/src/main/java/com/example/yeseul/movieapp/view/adapter/OnItemClickListener.java b/app/src/main/java/com/example/yeseul/movieapp/view/adapter/OnItemClickListener.java index 3077fee..df23324 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/view/adapter/OnItemClickListener.java +++ b/app/src/main/java/com/example/yeseul/movieapp/view/adapter/OnItemClickListener.java @@ -1,5 +1,7 @@ package com.example.yeseul.movieapp.view.adapter; +/* + * RecyclerView ItemClick Event 정의 */ public interface OnItemClickListener { void onItemClick(int position); diff --git a/app/src/main/java/com/example/yeseul/movieapp/view/main/MainActivity.java b/app/src/main/java/com/example/yeseul/movieapp/view/main/MainActivity.java index 574b81f..9b2a75b 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/view/main/MainActivity.java +++ b/app/src/main/java/com/example/yeseul/movieapp/view/main/MainActivity.java @@ -1,5 +1,10 @@ package com.example.yeseul.movieapp.view.main; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; @@ -12,15 +17,32 @@ import android.view.inputmethod.EditorInfo; import com.example.yeseul.movieapp.R; +import com.example.yeseul.movieapp.data.model.MovieSortByDictionary; +import com.example.yeseul.movieapp.data.model.MovieSortStrategy; +import com.example.yeseul.movieapp.data.model.MovieStrategyFactory; import com.example.yeseul.movieapp.data.source.movie.MovieRepository; import com.example.yeseul.movieapp.databinding.ActivityMovieBinding; import com.example.yeseul.movieapp.utils.KeyboardUtil; +import com.example.yeseul.movieapp.utils.NetworkStatusCheckUtil; import com.example.yeseul.movieapp.view.BaseActivity; + public class MainActivity extends BaseActivity implements MainContract.View { private MovieListAdapter adapter; + private BroadcastReceiver mNetworkStatusReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Log.e("Test", intent.getAction() + " state : " + NetworkStatusCheckUtil.checkCurrentNetworkStatus(MainActivity.this)); + // 네트워크 상태 변경 Action 이 발생한 경우 + if(intent.getAction().equals(NetworkStatusCheckUtil.NETWORK_STATUS_CHANGE_ACTION)) { + // 현재 인터넷 상태를 설정 + presenter.setNetworkStatus(NetworkStatusCheckUtil.checkCurrentNetworkStatus(MainActivity.this)); + } + } + }; + @Override protected int getLayoutId() { return R.layout.activity_movie; @@ -28,6 +50,8 @@ protected int getLayoutId() { @Override protected MainPresenter getPresenter() { + // Presenter 에서 사용할 Repository 객체는 싱글턴으로 작성 + // BaseActivity 에서 Presenter 는 미리 설정 return new MainPresenter(this, MovieRepository.getInstance()); } @@ -35,15 +59,47 @@ protected MainPresenter getPresenter() { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // RecyclerViewAdapter 설정 adapter = new MovieListAdapter(this); + + // Presenter 에서 View 를 거치는 과정을 생략하기 위해 + // Adapter 를 Presenter 에 등록 presenter.setAdapterView(adapter); presenter.setAdapterModel(adapter); + // xml 에서 변수로 설정해놓은 presenter 설정 binding.setPresenter(presenter); initView(); + } - presenter.onViewCreated(); + @Override + protected void onResume() { + super.onResume(); + // callback 형태로 View 가 설정된 후 호출 + presenter.onViewAttached(); + registerNetworkReceiver(); + } + + @Override + protected void onPause() { + super.onPause(); + // view 의 사용이 끝난 후 + presenter.onViewDetached(); + unregisterNetworkReceiver(); + } + + /* + * receiver 등록하기 */ + private void registerNetworkReceiver() { + IntentFilter intentFilter = new IntentFilter(NetworkStatusCheckUtil.NETWORK_STATUS_CHANGE_ACTION); + registerReceiver(mNetworkStatusReceiver, intentFilter); + } + + /* + * receiver 제거하기 */ + private void unregisterNetworkReceiver() { + unregisterReceiver(mNetworkStatusReceiver); } private void initView() { @@ -56,7 +112,7 @@ private void initView() { binding.recyclerMovie.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); // 최하단 스크롤 감지 - binding.recyclerMovie.setOnScrollListener(new RecyclerView.OnScrollListener(){ + binding.recyclerMovie.addOnScrollListener(new RecyclerView.OnScrollListener(){ @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); @@ -67,7 +123,8 @@ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newStat }); // 키보드 검색 버튼 리스너 등록 - binding.searchBox.etSearch.setOnEditorActionListener((v, actionId, event) -> { + binding.searchBox.etSearch.setOnEditorActionListener( (v, actionId, event) -> { + // IME_ACTION_SEARCH 와 View 에 Option 을 같게 설정해줘야 함 if(actionId == EditorInfo.IME_ACTION_SEARCH){ onSearchButtonClicked(); return true; @@ -77,8 +134,25 @@ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newStat // 검색 버튼 리스너 등록 binding.searchBox.btnSubmit.setOnClickListener(v -> onSearchButtonClicked()); + + // 정렬 버튼 리스너 등록 + binding.searchBox.btnOrder.setOnClickListener(v ->onOrderSelectButtonClicked()); } + private void onOrderSelectButtonClicked() { + + final String[] items = getResources().getStringArray(R.array.search_order); + + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + dialogBuilder.setTitle(getString(R.string.order_title)) + .setItems(items, (di, i) -> { + binding.searchBox.btnOrder.setText(items[i]); + presenter.onOrderSelectionChanged(MovieStrategyFactory.create(i)); + di.dismiss(); + }); + + dialogBuilder.show(); + } /** * 키보드 검색 혹은 검색 버튼 눌렀을 때 호출 */ private void onSearchButtonClicked(){ @@ -87,9 +161,13 @@ private void onSearchButtonClicked(){ String searchKey = binding.searchBox.etSearch.getText().toString(); if(!TextUtils.isEmpty(searchKey)) { + // Event 를 Presenter 에 위임 presenter.onSearchButtonClicked(searchKey); + // RecyclerView 위치를 최상단으로 설정 binding.recyclerMovie.scrollToPosition(0); + // View 값 설정 binding.emptyView.setText(""); + // 키보드 닫기 KeyboardUtil.closeKeyboard(this, binding.searchBox.etSearch); } else { @@ -112,6 +190,7 @@ public void onSearchResultEmpty(String searchKey) { @Override public void startMovieDetailPage(String linkUrl) { + // 다음 탭으로 넘어가기 CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder() .setToolbarColor(getResources().getColor(R.color.colorPrimary)) .build(); diff --git a/app/src/main/java/com/example/yeseul/movieapp/view/main/MainContract.java b/app/src/main/java/com/example/yeseul/movieapp/view/main/MainContract.java index ac42353..621eb2c 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/view/main/MainContract.java +++ b/app/src/main/java/com/example/yeseul/movieapp/view/main/MainContract.java @@ -1,25 +1,43 @@ package com.example.yeseul.movieapp.view.main; +import com.example.yeseul.movieapp.data.model.MovieSortStrategy; import com.example.yeseul.movieapp.pojo.Movie; import com.example.yeseul.movieapp.view.BasePresenter; import com.example.yeseul.movieapp.view.BaseView; import com.example.yeseul.movieapp.view.adapter.AdapterContract; +/** + * View - Presenter 는 1:1 관계 + * 각 객체의 공통 기능 설정 + */ public interface MainContract { interface View extends BaseView { + /* + * 검색 결과가 없을 때*/ void onSearchResultEmpty(String searchKey); + /* + * 영화를 선택했을 때 다음 화면으로 넘어가는 기능 */ void startMovieDetailPage(String linkUrl); } interface Presenter extends BasePresenter { + /* View 에서 발생하는 이벤트 처리는 Presenter 에 위임*/ void onSearchButtonClicked(String searchKey); + /* Presenter 에서 Adapter 를 가지고 View 갱신함 */ void setAdapterView(AdapterContract.View adapterView); + /* Presenter 에서 Adapter 의 데이터를 조작함 */ void setAdapterModel(AdapterContract.Model adapterModel); + + /* Network 상태를 Presenter 의 Observer 를 이용하여 관찰 */ + void setNetworkStatus(boolean currentStatus); + + /* 정렬 상태가 변경되었을 경우 */ + void onOrderSelectionChanged(MovieSortStrategy sortMethod); } } diff --git a/app/src/main/java/com/example/yeseul/movieapp/view/main/MainPresenter.java b/app/src/main/java/com/example/yeseul/movieapp/view/main/MainPresenter.java index b57c015..8c1a447 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/view/main/MainPresenter.java +++ b/app/src/main/java/com/example/yeseul/movieapp/view/main/MainPresenter.java @@ -3,10 +3,13 @@ import android.annotation.SuppressLint; import android.databinding.ObservableBoolean; import android.databinding.ObservableField; +import android.util.Log; +import com.example.yeseul.movieapp.data.model.MovieSortStrategy; import com.example.yeseul.movieapp.data.source.movie.MovieRepository; import com.example.yeseul.movieapp.mapper.MovieMapper; import com.example.yeseul.movieapp.pojo.Movie; +import com.example.yeseul.movieapp.utils.NetworkStatusCheckUtil; import com.example.yeseul.movieapp.view.adapter.AdapterContract; import java.util.List; @@ -27,16 +30,31 @@ public class MainPresenter implements MainContract.Presenter { private int currentPage = 0; // 현재 페이지 index private boolean isEndOfPage = false; // 페이지 끝 flag + private boolean isNetworkConnected = false; + public MainPresenter(MainContract.View view, MovieRepository repository) { this.view = view; this.repository = repository; } + /* BasePresenter */ + @Override + public void onViewAttached() { + // 네트워크 초기 상태 설정 + isNetworkConnected = NetworkStatusCheckUtil.checkCurrentNetworkStatus((MainActivity)view); + } + + @Override + public void onViewDetached() { + // do Something + } + @Override - public void onViewCreated() { - // do nothing + public void setNetworkStatus(boolean currentStatus) { + this.isNetworkConnected = currentStatus; } + /* BasePresenter */ @Override public void loadItems(boolean isRefresh) { @@ -47,18 +65,26 @@ public void loadItems(boolean isRefresh) { adapterModel.clearItems(); } + // 인터넷 연결이 안되어있는 경우 + if(!isNetworkConnected) { + view.makeToast("인터넷 상태를 확인해 주세요 !"); + return; + } + // 마지막 페이지가 아니고 로딩중 아닌 경우 getMovieList 호출 if(!isLoading.get() && !isEndOfPage) { getMovieList(); } } + /* MainContract.Presenter */ @Override public void onSearchButtonClicked(String searchKey) { this.searchKey = searchKey; loadItems(true); } + /* MainContract.Presenter */ @Override public void setAdapterView(AdapterContract.View adapterView) { this.adapterView = adapterView; @@ -66,23 +92,43 @@ public void setAdapterView(AdapterContract.View adapterView) { view.startMovieDetailPage(this.adapterModel.getItem(position).getLinkUrl())); } + /* MainContract.Presenter */ @Override public void setAdapterModel(AdapterContract.Model adapterModel) { this.adapterModel = adapterModel; } + @Override + public void onOrderSelectionChanged(MovieSortStrategy sortStrategy) { + if(sortStrategy != null) { + adapterModel.updateItems(sortStrategy.sort(adapterModel.getItemList())); + } + } + @SuppressLint("CheckResult") private void getMovieList(){ + if(!isNetworkConnected) { + view.makeToast("인터넷 상태를 확인해 주세요 !"); + return; + } + + // RXJava 특성상 Subscribe 하고 있어서 + // activity_movie 의 progressBar Visibility 가 변경된다. isLoading.set(true); + // repository 에 접근하는 객체는 Singleton 으로 받아옴 + // searchMovies 의 return value 가 Single 객체이므로 딱 한번만 데이터를 발행 + // subscribe 에서 발행된 데이터를 Consumer 가 처리하는 구조 repository.searchMovies(MovieMapper.toRequest(searchKey, PAGE_UNIT, (PAGE_UNIT * currentPage++) + 1)) .observeOn(AndroidSchedulers.mainThread()) .subscribe(response -> { + // 받아온 SearchMovieResponse 는 어디서 처리하는거지 - // 로딩 flag OFF + // 로딩 flag OFF (Observing) isLoading.set(false); + // 받아온 결과 목록 List movieList = response.getMovieList(); if(movieList.size() == 0){ // 검색 결과가 없는 경우 @@ -98,6 +144,8 @@ private void getMovieList(){ isEndOfPage = true; } + // 선택한 정렬 옵션으로 정렬 + // 어댑터에 리스트 추가 adapterModel.addItems(movieList); @@ -111,4 +159,7 @@ private void getMovieList(){ view.onSearchResultEmpty(searchKey); }); } + + + } diff --git a/app/src/main/java/com/example/yeseul/movieapp/view/main/MovieListAdapter.java b/app/src/main/java/com/example/yeseul/movieapp/view/main/MovieListAdapter.java index 534964c..b50caaf 100644 --- a/app/src/main/java/com/example/yeseul/movieapp/view/main/MovieListAdapter.java +++ b/app/src/main/java/com/example/yeseul/movieapp/view/main/MovieListAdapter.java @@ -21,18 +21,25 @@ public MovieListAdapter(Context context) { @Override protected void onBindView(ViewHolder holder, int position) { + // ViewHolder 에서 사용할 item 을 binding 해준다. holder.binding.setItem(itemList.get(position)); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int i) { + // ViewHolder 바인딩 View view = LayoutInflater.from(context).inflate(R.layout.holder_movie_item, parent, false); return new ViewHolder(view); } - public class ViewHolder extends RecyclerView.ViewHolder{ + // static 의미 : nested class 로 상위 클래스의 메소드, 변수를 사용하지 않겠다고 명시 + // non-static 인 경우엔 상위 클래스의 reference 를 가지고 있음 -> Adapter 가 죽어도 ViewHolder 에서 Adapter 를 가지고 있는 경우 memory leak 발생 + // adapter 가 한 곳에서만 사용된다면 non-static 도 상관없다. + public static class ViewHolder extends RecyclerView.ViewHolder{ + // binding 만 하고 onBindView 할 때 item 을 설정해주면 + // RXJava 를 이용해서 자동적으로 ViewHolder 의 값이 변경되게 함 HolderMovieItemBinding binding; public ViewHolder(@NonNull View itemView) { diff --git a/app/src/main/res/layout/holder_movie_item.xml b/app/src/main/res/layout/holder_movie_item.xml index 0ad3707..66ea5b3 100644 --- a/app/src/main/res/layout/holder_movie_item.xml +++ b/app/src/main/res/layout/holder_movie_item.xml @@ -38,14 +38,15 @@ android:lineSpacingExtra="@dimen/padding_ultra_small" boldText="@{item.title}"/> + + android:numStars="5" + rating="@{item.userRating}"/> +