Recyclerview with Retrofit and ListAdapter using MVVM
안드로이드/ListView, RecyclerView 2022. 8. 28. 18:51MVVM 구조의 핵심 사항만 소스코드로 예시한다.
전체 소스코드는 Github 에서 받아서 Project 에서 New Module을 생성하고 src 폴더를 덮어쓰기 해도 되고, 참고하면서 소스코드를 작성하는 것도 방법이다.
암호화함수를 모아둔 Class 는 오픈하지 않았으므로 전체적인 로직만 참고하면 도움될 것이다.
AddressRepository.java
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.link2me.android.common.CipherUtils;
import com.link2me.android.rview_ex3.model.AddressResult;
import com.link2me.android.rview_ex3.network.RetrofitAdapter;
import com.link2me.android.rview_ex3.network.RetrofitService;
import com.link2me.android.rview_ex3.network.RetrofitURL;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class AddressRepository {
private RetrofitService service;
private MutableLiveData<AddressResult> mutableLiveData;
public AddressRepository() {
mutableLiveData = new MutableLiveData<>();
service = RetrofitAdapter.getClient(RetrofitURL.BaseUrl).create(RetrofitService.class);
}
public void addressDataRepo(String search) {
String keyword = CipherUtils.encryptAES(CipherUtils.URLkey());
service.GetAddrData(keyword, search).enqueue(new Callback<AddressResult>() {
@Override
public void onResponse(Call<AddressResult> call, Response<AddressResult> response) {
mutableLiveData.postValue(response.body());
}
@Override
public void onFailure(Call<AddressResult> call, Throwable t) {
mutableLiveData.postValue(null);
}
});
}
public LiveData<AddressResult> getAddressResultLiveData(){
return mutableLiveData;
}
}
|
AddressViewModel.java
public class AddressViewModel extends ViewModel {
private AddressRepository repository;
private LiveData<AddressResult> _liveData;
public AddressViewModel() {
}
public void getAllAddressData(String search){
repository.addressDataRepo(search);
}
public LiveData<AddressResult> get_liveData(){
if(_liveData == null){
repository = new AddressRepository();
_liveData = repository.getAddressResultLiveData();
}
return _liveData;
}
}
|
AddressListAdapter.java
/***
* https://www.youtube.com/watch?v=xPPMygGxiEo 동영상 시청하면 개념 이해하는데 엄청 도움됨
*/
// RecyclerView.Adapter 대신에 ListAdapter<T, VH> 를 상속한다.
// T는 리사이클러뷰가 화면에 표시할 데이터의 타입이다.
public class AddressListAdapter extends ListAdapter<Address_Item, AddressListAdapter.ViewHolder> {
private final String TAG = this.getClass().getSimpleName();
Context context;
// 인터페이스 선언 -------------------------------------------------------------
private OnItemClickListener mListener;
public interface OnItemClickListener {
void onItemClicked(Address_Item item, int position);
}
public void setOnItemClickListener(OnItemClickListener listener){
mListener = listener;
}
// 인터페이스 ----------------------------------------------------------------
public AddressListAdapter(Context context, @NonNull DiffUtil.ItemCallback<Address_Item> diffCallback) {
super(diffCallback);
this.context = context;
}
public void selectAll(int list_count){ // checkbox 전체 선택
for(int i=0;i < list_count;i++){
getItem(i).setCheckBoxState(true);
}
notifyDataSetChanged();
}
public void unselectall(int list_count){ // checkbox 전체 해제
for(int i=0;i < list_count;i++){
getItem(i).setCheckBoxState(false);
}
notifyDataSetChanged();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemAddressBinding binding = ItemAddressBinding.inflate(LayoutInflater.from(parent.getContext()),parent,false);
return new ViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
// 실제로 데이터를 표시하는 부분
//Address_Item currentItem = rvItemList.get(position);
Address_Item currentItem = getItem(position);
holder.bindItem(currentItem, position);
}
public class ViewHolder extends RecyclerView.ViewHolder{
ItemAddressBinding itemBinding;
public ViewHolder(@NonNull ItemAddressBinding binding) {
super(binding.getRoot());
itemBinding = binding;
}
void bindItem(Address_Item item, int position){
String photoURL = RetrofitURL.BaseUrl + "photos/" + item.getPhoto();
if(photoURL.contains("null")){
Glide.with(context).asBitmap().load(R.drawable.photo_base).into(itemBinding.profileImage);
} else {
Glide.with(context).asBitmap().load(photoURL).into(itemBinding.profileImage);
}
itemBinding.childName.setText(item.getUserNM());
itemBinding.childMobileNO.setText(PhoneNumberUtils.formatNumber(item.getMobileNO()));
itemBinding.childOfficeNO.setText(PhoneNumberUtils.formatNumber(item.getTelNO()));
itemBinding.profileImage.setOnClickListener(view1 -> {
if(mListener != null){
mListener.onItemClicked(item,position); // 클릭의 결과로 값을 전달
}
});
if (isCheckFlag == false) {
itemBinding.callBtn.setVisibility(View.VISIBLE);
itemBinding.callBtn.setOnClickListener(view -> builder.show());
itemBinding.listcellCheckbox.setVisibility(View.GONE);
itemBinding.childLayout.setOnClickListener(view ->
Toast.makeText(context, "상세보기를 눌렀습니다 ===" + position + " , idx :" + item.getIdx(), Toast.LENGTH_SHORT).show());
itemBinding.childLayout.setOnLongClickListener(v -> {
isCheckFlag = true;
constraintLayout.setVisibility(View.VISIBLE);
notifyDataSetChanged();
return true;
});
} else {
itemBinding.callBtn.setVisibility(View.GONE);
//convertView.setClickable(false);
itemBinding.listcellCheckbox.setVisibility(View.VISIBLE);
itemBinding.listcellCheckbox.setTag(position); // This line is important.
// 체크 박스 클릭하면 CheckBoxState 에 반영한다. setOnCheckedChangeListener 대신 사용
itemBinding.listcellCheckbox.setOnClickListener(v -> {
if(getItem(position).getCheckBoxState() == true){
getItem(position).setCheckBoxState(false);
Log.d("checkbox","position : "+ position + " checkBoxState === "+ getItem(position).getCheckBoxState());
} else {
getItem(position).setCheckBoxState(true);
Log.d("checkbox","position : "+ position + " checkBoxState === "+ getItem(position).getCheckBoxState());
}
});
itemBinding.childLayout.setOnClickListener(v -> {
if(itemBinding.listcellCheckbox.isChecked() == false){
itemBinding.listcellCheckbox.setChecked(true);
getItem(position).setCheckBoxState(true);
//notifyDataSetChanged();
Log.d("checklist","position : "+ position + " checkBoxState === "+ getItem(position).getCheckBoxState());
} else {
itemBinding.listcellCheckbox.setChecked(false);
getItem(position).setCheckBoxState(false);
//notifyDataSetChanged();
Log.d("checklist","position : "+ position + " checkBoxState === "+ getItem(position).getCheckBoxState());
}
});
}
// 재사용 문제 해결
if(getItem(position).getCheckBoxState() == true){
itemBinding.listcellCheckbox.setChecked(true);
Log.d("ReUse","position : " + position + " checkBoxState === " + getItem(position).getCheckBoxState());
} else {
itemBinding.listcellCheckbox.setChecked(false);
Log.d("ReUse","position : "+position + " checkBoxState ===" + getItem(position).getCheckBoxState());
}
}
}
}
|
AddressActivity.java
public class AddressActivity extends AppCompatActivity implements AddressListAdapter.OnItemClickListener {
private final String TAG = this.getClass().getSimpleName();
Context mContext;
private ActivityAddressBinding binding;
private AddressViewModel viewModel;
private ArrayList<Address_Item> addressItemList = new ArrayList<>(); // 서버 전체 데이터 리스트
private ArrayList<Address_Item> searchItemList = new ArrayList<>(); // 검색한 데이터 리스트
private RecyclerView mRecyclerView;
private AddressListAdapter mAdapter;
public static ConstraintLayout constraintLayout;
public static boolean isCheckFlag = false;
public CheckBox checkAll;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_address);
binding = ActivityAddressBinding.inflate(getLayoutInflater());
View view = binding.getRoot();
setContentView(view);
mContext = AddressActivity.this;
initView();
}
private void initView() {
// 동기식 데이터 처리에서는 순서가 중요함.
viewModelProvider();
buildRecyclerView();
getAddressList(); // 서버 데이터 가져오기
setButtons();
}
private void buildRecyclerView() {
mRecyclerView = binding.rvAddress;
mRecyclerView.setHasFixedSize(true);
LinearLayoutManager manager = new LinearLayoutManager(mContext);
//mAdapter = new AddressViewAdapter(mContext,searchItemList); // 객체 생성
mAdapter = new AddressListAdapter(mContext, Address_Item.itemCallback); // DiffUtil을 넣은 어댑터를 생성
//mAdapter.submitList(searchItemList); // 서버에서 데이터를 가져온 이 후 코드에 추가하면 된다.
DividerItemDecoration decoration = new DividerItemDecoration(mContext,manager.getOrientation());
mRecyclerView.addItemDecoration(decoration);
mRecyclerView.setLayoutManager(manager);
mRecyclerView.setAdapter(mAdapter);
mAdapter.setOnItemClickListener(this);
}
private void viewModelProvider() {
// viewModel init
viewModel = new ViewModelProvider(this).get(AddressViewModel.class);
// viewModel observe
viewModel.get_liveData().observe(this, new Observer<AddressResult>() {
@Override
public void onChanged(AddressResult addressResult) {
if(addressResult.getStatus().contains("success")){
addressItemList.clear(); // 서버에서 가져온 데이터 초기화 (전체 List)
searchItemList.clear(); // 서버에서 가져온 데이터 초기화 (검색 List)
for(Address_Item item : addressResult.getAddrinfo()){
addressItemList.add(item);
searchItemList.add(item);
}
Log.e(TAG, "viewModel observe");
mAdapter.submitList(searchItemList);
} else {
Utils.showAlert(mContext,addressResult.getStatus(),addressResult.getMessage());
}
}
});
}
private void getAddressList() {
String search = "";
viewModel.getAllAddressData(search);
}
private void setButtons(){
isCheckFlag = false;
constraintLayout = binding.listviewConstraint2;
binding.search.setSubmitButtonEnabled(false);
binding.search.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
// 문자열 입력을 완료했을 때 문자열 반환
// 쿼리가 비어있을 때에는 호출되지 않는다.
//viewModel.getAllAddressData(query); // 서버 재접속 후 데이터 가져오기
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
// 문자열이 변할 때마다 바로바로 문자열 반환
filter(newText); // 전체 데이터 가져온 후 로컬에서 검색 처리
// if(newText.trim().length() == 0) {
// viewModel.getAllAddressData("");
// }
return true;
}
});
if (isCheckFlag == false) {
constraintLayout.setVisibility(View.GONE);
} else if (isCheckFlag == true) {
constraintLayout.setVisibility(View.VISIBLE);
}
// all checkbox
checkAll = binding.lvCheckboxAll;
binding.lvCheckboxAll.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (checkAll.isChecked() == true) {
mAdapter.selectAll(searchItemList.size());
} else {
mAdapter.unselectall(searchItemList.size());
}
});
binding.btnCancel.setOnClickListener(v -> {
isCheckFlag = false;
constraintLayout.setVisibility(View.GONE);
mAdapter.unselectall(searchItemList.size());
checkAll.setChecked(false); // 전체 선택 체크박스 해제
});
}
// Filter Class
public void filter(String charText) {
charText = charText.toLowerCase(Locale.getDefault());
searchItemList.clear();
if (charText.length() == 0) {
searchItemList.addAll(addressItemList);
} else {
for (Address_Item wp : addressItemList) {
if(Utils.isNumber(charText)){ // 숫자여부 체크
if(wp.getMobileNO().contains(charText) || wp.getTelNO().contains(charText)){
// 휴대폰번호 또는 사무실번호에 숫자가 포함되어 있으면
searchItemList.add(wp);
}
} else {
String iniName = HangulUtils.getHangulInitialSound(wp.getUserNM(), charText);
if (iniName.indexOf(charText) >= 0) { // 초성검색어가 있으면 해당 데이터 리스트에 추가
searchItemList.add(wp);
} else if (wp.getUserNM().toLowerCase(Locale.getDefault()).contains(charText)) {
searchItemList.add(wp);
}
}
}
}
mAdapter.notifyDataSetChanged();
}
@Override
public void onItemClicked(Address_Item item, int position) {
Toast.makeText(mContext, "position : "+position + " clieck item name : "+item.getUserNM(), Toast.LENGTH_SHORT).show();
}
}
|
Github Source Code : https://github.com/jsk005/JavaProjects/tree/master/recyclerview_mvvm
'안드로이드 > ListView, RecyclerView' 카테고리의 다른 글
[java] RecyclerView ListAdapter DiffUtil (0) | 2022.08.24 |
---|---|
[Java] RecyclerView view binding (0) | 2022.05.23 |
[코틀린] expandable recyclerview 예제 (0) | 2021.06.03 |
RecyclerView 역순으로 리스트 출력하기 (0) | 2020.08.05 |
RecyclerView Checkbox 처리 및 Interface 처리 (0) | 2020.07.17 |