728x90

하나의 클래스를 다른 클래스에 위임하도록 선언하여 위임된 클래스가 가지는 인터페이스 메소드를 참조 없이 호출할 수 있도록 생성해주는 기능이다.

 

interface Fruit를 구현하고 있는 class Apple이 있다면, 
Fruit에서 정의하고 있는 Apple의 모든 메소드를 클래스 GreenApple로 위임할 수 있다. 
즉, GreenApple은 Apple이 가지는 모든 Fruit의 메소드를 가지며, 이를 클래스 위임(Class delegation)이라고 한다.

interface Fruit {
    val name: String
    val color: String
    fun bite()
}
 
class Apple: Fruit {
    override val name: String
        get() = "사과"
    override val color: String
        get() = "빨간색"
 
    override fun bite() {
        print("사과 아삭~ 아삭~")
    }
}
 
// 클래스 위임 : 사용할 기능은 그대로 사용하고 새로 정의할 것만 override로 재정의
class GreenApple(
    private val apple: Apple
) : Fruit by apple {
    override val color: String
        get() = "초록색"
}

 

fun main() {
    val greenApple = GreenApple(Apple())
    println(greenApple.color)
    println(greenApple.name)
    greenApple.bite()
}

 

 

'안드로이드 > Kotlin 문법' 카테고리의 다른 글

코틀린 제네릭 공변, 반공변  (0) 2024.02.04
[코틀린] ArrayList, mutableListOf  (0) 2021.06.14
[코틀린] Inner Class  (0) 2020.08.21
[코틀린] Nested Class (중첩 클래스)  (0) 2020.08.21
코틀린 클래스  (0) 2020.05.06
블로그 이미지

Link2Me

,
728x90

Kotlin에서 List, Map, Set같은 Collections 클래스를 사용할 때, List<String> 이런식으로 제네릭 타입을 지정할 수 있다.

제네릭을 사용하면 타입 안정성을 얻을 수 있다.
하지만 제네릭으로 지정한 타입 이외에도 좀 더 유연하게 사용하고 싶을 수 있다.
그래서 코틀린에서는 공변성과 반공변성을 허용한다.

 

package generic
 
abstract class Animal(val name: String)
 
abstract class Fish(name: String) : Animal(name)
 
// 금붕어
class GoldFish(name: String) : Fish(name)
 
// 잉어
class Carp(name: String) : Fish(name)

 

package generic
 
fun main(){
    val goldFishCage = Cage<GoldFish>()
    goldFishCage.put(GoldFish("금붕어")) // Type Casting없이 바로 금붕어를 가져올 수 있다.
 
    val cage = Cage<Fish>()
    cage.moveFrom(goldFishCage)
 
    val fish: Fish = cage.getFirst()
    // Cage<Fish>에서 데이터를 가져오면 GoldFish인지 Carp인지 모른다.
}
 
/***
 * 제네릭 클래스 : 타입 파라미터를 사용한 클래스
 * 코틀린에서는 Raw 타입 사용이 불가능하다.
 * Raw Type : 제네릭 클래스에서 타입 매개변수를 사용하지 않고 인스턴스화 하는 것
 * in-variant(무공변) : 타입 파라미터끼리는 상속관계이더라도, 제네릭 클래스 간에는 상속관계가 없다는 의미
 * co-variant(공변) : 타입 파라미터간의 상속관계가 제네릭 클래스에도 동일하게 유지된다는 의미
 * 코틀린에서는 타입 파리미터 앞에 out 변성 어노테이션을 사용한다.
 * contra_variant(반공변) : 타입 파라미터간의 상속관계가 제네릭 클래스에서는 반대로 유지된다는 의미
 * 코틀린에서는 타입 파리미터 앞에 in 변성 어노테이션을 사용한다.
 */
class Cage<T : Animal> {
    private val animals: MutableList<T> = mutableListOf()
 
    fun getFirst() : T {
        return animals.first();
    }
 
    fun getAll() : List<T> {
        return  this.animals
    }
 
    fun put(animal: T) {
        this.animals.add(animal)
    }
 
    fun moveFrom(otherCage: Cage<out T>) {
        // out을 붙이면 moveFrom 함수를 호출할 때 Cage는 공변하게 된다.
        // out을 붙이면, otherCage로부터 데이터를 꺼낼 수만 있다.
        this.animals.addAll(otherCage.animals)
    }
 
    fun moveTo(otherCage: Cage<in T>){
        // in을 붙인 otherCage는 데이터를 받을 수만 있다.
        otherCage.animals.addAll(this.animals)
    }
}

 

 

 

'안드로이드 > Kotlin 문법' 카테고리의 다른 글

코틀린 클래스 위임(Class Delegation)  (1) 2024.02.06
[코틀린] ArrayList, mutableListOf  (0) 2021.06.14
[코틀린] Inner Class  (0) 2020.08.21
[코틀린] Nested Class (중첩 클래스)  (0) 2020.08.21
코틀린 클래스  (0) 2020.05.06
블로그 이미지

Link2Me

,

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

728x90

샘플 코드만 적어둔다.

Android 네이버 Map 라이브러리를 이용하여 검색(Search)으로 위치 이동과 더불어 지도 레벨을 지정해야 할 때 필요한 코드이다.

 

private fun LocationAt(latitude: Double, longitude: Double) {
    mNaverMap?.locationTrackingMode = LocationTrackingMode.None
    val coord = LatLng(latitude, longitude)
    val cameraPosition = CameraPosition(coord, 16.0)
    mNaverMap.cameraPosition = cameraPosition
    val cameraUpdate = CameraUpdate.scrollTo(coord).animate(CameraAnimation.Easing, 300)
    mNaverMap!!.moveCamera(cameraUpdate)
}

 

 

블로그 이미지

Link2Me

,
728x90

Android Room 예제를 검색하면 가장 많이 검색되는 것이 Simple Note 를 다룬 것이다.

아래 동영상 이외에도 대부분의 게시글, 동영상을 보면 모두 Simple Note를 약간씩 다르게 다루고 있지만 본질은 모두 동일하더라.

https://www.youtube.com/watch?v=xPPMygGxiEo 

 

이 자료를 토대로 하여 코드를 약간 손을 보고 디자인 부분을 수정하고 검색 기능을 추가하여 자료를 정리했다.

- CoordinatorLayout 과 MaterialToolbar 사용

- Menu SearchView 기능 추가 및 Room Database 검색 기능 추가

- ListAdapter 와 RecyclerView.Adapter 두가지 구현

- View Binding 적용

 

package com.link2me.android.notesample.view.activity;
 
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
 
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
 
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.link2me.android.notesample.R;
import com.link2me.android.notesample.databinding.ActivityMainBinding;
import com.link2me.android.notesample.model.Note;
import com.link2me.android.notesample.view.adapter.NoteViewAdapter;
import com.link2me.android.notesample.viewmodel.MyFactory;
import com.link2me.android.notesample.viewmodel.NoteViewModel;
 
import java.util.List;
 
public class MainActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();
    private Context mContext;
    private ActivityMainBinding binding;
 
    public static final int ADD_NOTE_REQUEST = 1;
    public static final int EDIT_NOTE_REQUEST = 2;
 
    private NoteViewModel viewModel;
 
    private RecyclerView mRecyclerView;
    //private NoteAdapter mAdapter;
    private NoteViewAdapter mAdapter;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        View view = binding.getRoot();
        setContentView(view);
 
        mContext = MainActivity.this;
 
        initView();
    }
 
    private void initView() {
        // 동기식 데이터 처리에서는 순서가 중요함.
        setToolbar();
        buildRecyclerView();
        initViewModel();
        setButtons();
    }
 
    private void setToolbar() {
        // activity에 툴바를 추가하려면 우선 기본 앱바(AppBar)부터 비활성화해야 한다.
        // 기존 actionBar는 theme 에서 NoActionBar로 설정하여 제거하고 툴바를 생성한다.
        setSupportActionBar(binding.mainToolbar);
        // Toolbar의 왼쪽에 버튼을 추가하고 버튼의 아이콘을 바꾼다.
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_dehaze_24);
 
        // 타이틀명 변경
        if (getSupportActionBar() != null) {
            getSupportActionBar().setTitle("TODO LIST");
            binding.mainToolbar.setTitleTextColor(Color.WHITE);
        }
 
    }
 
    private void initViewModel() {
        // viewModel init
        //viewModel = new ViewModelProvider(this, new MyFactory(getApplication())).get(NoteViewModel.class);
        viewModel = new ViewModelProvider(this).get(NoteViewModel.class);
 
        // viewModel observe
        viewModel.getAllNotes().observe(thisnew Observer<List<Note>>() {
            @Override
            public void onChanged(@Nullable List<Note> notes) {
                //mAdapter.submitList(notes);
                mAdapter.setNotes(notes);
                for(Note note:notes){
                    Log.e(TAG,note.getId() + ", " + note.getTitle() + ", " + note.getDescription() + ", " + note.getPriority());
                }
                Log.e(TAG, "note_db size : " + notes.size());
            }
        });
 
    }
 
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.e(TAG,"onNewIntent");
    }
 
    private void buildRecyclerView() {
        mRecyclerView = binding.recyclerView;
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mRecyclerView.setHasFixedSize(true);
        LinearLayoutManager manager = new LinearLayoutManager(mContext);
 
//        mAdapter = new NoteAdapter();
        mAdapter = new NoteViewAdapter();
 
        DividerItemDecoration decoration = new DividerItemDecoration(mContext, manager.getOrientation());
        mRecyclerView.addItemDecoration(decoration);
        mRecyclerView.setLayoutManager(manager);
        mRecyclerView.setAdapter(mAdapter);
    }
 
    private void setButtons() {
        // 안드로이드 디자인 지원 라이브러리에는 FloatingActionButton 클래스가 제공된다.
        FloatingActionButton buttonAddNote = binding.buttonAddNote;
        buttonAddNote.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, AddNoteActivity.class);
                startActivityForResult(intent, ADD_NOTE_REQUEST);
            }
        });
 
        new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0,
                ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
            @Override
            public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
                return false;
            }
 
            @Override
            public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
                viewModel.delete(mAdapter.getNoteAt(viewHolder.getBindingAdapterPosition()));
                Toast.makeText(MainActivity.this"Note deleted", Toast.LENGTH_SHORT).show();
            }
        }).attachToRecyclerView(mRecyclerView);
 
        mAdapter.setOnItemClickListener(new NoteViewAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(Note note) {
                Intent intent = new Intent(MainActivity.this, AddNoteActivity.class);
                intent.putExtra(AddNoteActivity.EXTRA_ID, note.getId());
                intent.putExtra(AddNoteActivity.EXTRA_TITLE, note.getTitle());
                intent.putExtra(AddNoteActivity.EXTRA_DESCRIPTION, note.getDescription());
                intent.putExtra(AddNoteActivity.EXTRA_PRIORITY, note.getPriority());
                startActivityForResult(intent, EDIT_NOTE_REQUEST);
            }
        });
 
    }
 
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
 
        if (requestCode == ADD_NOTE_REQUEST && resultCode == RESULT_OK) {
            String title = data.getStringExtra(AddNoteActivity.EXTRA_TITLE);
            String description = data.getStringExtra(AddNoteActivity.EXTRA_DESCRIPTION);
            int priority = data.getIntExtra(AddNoteActivity.EXTRA_PRIORITY, 1);
 
            Note note = new Note(title, description, priority);
            viewModel.insert(note);
 
            Toast.makeText(this"메모가 저장되었습니다.", Toast.LENGTH_SHORT).show();
        } else if (requestCode == EDIT_NOTE_REQUEST && resultCode == RESULT_OK) {
            int id = data.getIntExtra(AddNoteActivity.EXTRA_ID, -1);
 
            if (id == -1) {
                Toast.makeText(this"Note can't be updated", Toast.LENGTH_SHORT).show();
                return;
            }
 
            String title = data.getStringExtra(AddNoteActivity.EXTRA_TITLE);
            String description = data.getStringExtra(AddNoteActivity.EXTRA_DESCRIPTION);
            int priority = data.getIntExtra(AddNoteActivity.EXTRA_PRIORITY, 1);
 
            Note note = new Note(title, description, priority);
            note.setId(id);
            viewModel.update(note);
 
            Toast.makeText(this"Note updated", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this"Note not saved", Toast.LENGTH_SHORT).show();
        }
    }
 
    /***
     * 메뉴는 프로젝트의 res/menu 폴더에 저장되는 XML 리소스 형식으로 정의할 수 있다.
     * 메뉴는 루트 노드에서 menu 태그와 일련의 item 태그로 구성된다.
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // 부모 activity 나 fragment 와 메뉴를 연관시켜야 하므로 항상 super.onCreateOptionsMenu를 호출해야 한다.
        super.onCreateOptionsMenu(menu);
        MenuInflater menuInflater = getMenuInflater();
        menuInflater.inflate(R.menu.main_menu, menu);
 
        // 검색 메뉴 등록
        MenuItem searchItem = menu.findItem(R.id.action_search);
        SearchView searchView = (SearchView) searchItem.getActionView();
 
        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                // 문자열 입력을 완료했을 때 문자열 반환
                if(query != null) {
                    searchDatabase(query);
                }
                return true;
            }
 
            @Override
            public boolean onQueryTextChange(String newText) {
                // 문자열이 변할 때마다 바로바로 문자열 반환
                if(newText != null) {
                    searchDatabase(newText);
                }
                return true;
            }
        });
 
        return true;
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.delete_all_notes:
                viewModel.deleteAllNotes();
                Toast.makeText(this"All notes deleted", Toast.LENGTH_SHORT).show();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }
 
    private void searchDatabase(String search){
        String searchQuery = "%" + search + "%";
 
        viewModel.searchDatabase(searchQuery).observe(thisnew Observer<List<Note>>() {
            @Override
            public void onChanged(List<Note> notes) {
                //mAdapter.submitList(notes);
                mAdapter.setNotes(notes);
            }
        });
    }
 
}

 

 

NoteViewModel.java

 
import android.app.Application;
 
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
 
import com.link2me.android.notesample.model.Note;
import com.link2me.android.notesample.repository.NoteRepository;
import com.link2me.android.notesample.view.activity.MainActivity;
 
import java.util.List;
 
public class NoteViewModel extends AndroidViewModel {
    private NoteRepository repository;
    private LiveData<List<Note>> allNotes;
 
    public NoteViewModel(@NonNull Application application) {
        super(application);
        repository = new NoteRepository(application);
        allNotes = repository.getAllNotes();
    }
 
    public void insert(Note note) {
        repository.insert(note);
    }
 
    public void update(Note note) {
        repository.update(note);
    }
 
    public void delete(Note note) {
        repository.delete(note);
    }
 
    public void deleteAllNotes() {
        repository.deleteAllNotes();
    }
 
    public LiveData<List<Note>> getAllNotes() {
        return allNotes;
    }
 
 
    public LiveData<List<Note>> searchDatabase(String searchQuery){
        return repository.searchDatabase(searchQuery);
    }
}

 

전체적인 폴더 구조는 아래와 같다.

 

전체 소스코드는 GitHub에 올려두었다.

https://github.com/jsk005/JavaProjects/tree/master/room_mvvm_search

 

GitHub - jsk005/JavaProjects: 활용 수준의 자바 기능 테스트

활용 수준의 자바 기능 테스트. Contribute to jsk005/JavaProjects development by creating an account on GitHub.

github.com

 

블로그 이미지

Link2Me

,
728x90

MVVM 구조의 핵심 사항만 소스코드로 예시한다.

전체 소스코드는 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(thisnew 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

 

GitHub - jsk005/JavaProjects: 자바 기능 테스트

자바 기능 테스트. Contribute to jsk005/JavaProjects development by creating an account on GitHub.

github.com

 

블로그 이미지

Link2Me

,
728x90

코틀린 기반으로 코드를 구현하면 쉽게 샘플 예제를 찾을 수 있다. 하지만 Java 코드 기반 자료는 찾기가 쉽지 않다.

제일 빠르게 찾는 방법은 유투브 동영상에서 검색하는 것이다.

https://www.youtube.com/watch?v=xPPMygGxiEo 

이 동영상 강좌 시청하면서 링크된 Github 샘플 자료를 참고하면 이해하는데 훨씬 빠를 것이다.

 

 

ListAdapter는 RecyclerView에 쓸 수 있는 어댑터이다.
기존의 어댑터와는 다르게 DiffUtil을 사용하여 비동기식 처리를 할 수 있다.
기존의 기본 어댑터는 mAdapter.notifyDataSetChanged(); 메소드를 사용하여 리스트의 변경처리를 했었는데, 
ListAdapter에서는 submitList(...) 를 사용하여 리스트가 변경되었음을 어댑터에게 알려줄 수 있다.

 

DiffUtil 클래스는 oldItem, newItem의 두 데이터셋을 비교하여 값이 변경된 부분만을 RecyclerView에게 알려줄 수 있다.

비교 결과, 다르다고 판단된 아이템 항목만 교체를 진행한다. 이것이 중요한 포인트이다.

 

아래 샘플 예제는 MVVM 패턴을 고려하여 작성한 코드는 아니며, 내용 숙지 차원에서 수정 부분 중심으로 적어둔다.

 

Java 코드로 구현한 Class

- 일단 코드가 참 길다.

- DiffUtil.ItemCallback 메소드 구현을 이곳에 하는 예제도 있고 RecyclerViewApdater 에 구현한 예제도 있다.

import android.graphics.Movie;
 
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
 
import java.util.Objects;
 
public class Address_Item {
    private String idx;
    private String userNM;
    private String mobileNO;
    private String telNO;
    private String photo;
    private Boolean checkBoxState;
 
    public Address_Item(String idx, String userNM, String mobileNO, String telNO, String photo, Boolean checkBoxState) {
        this.idx = idx;
        this.userNM = userNM;
        this.mobileNO = mobileNO;
        this.telNO = telNO;
        this.photo = photo;
        this.checkBoxState = checkBoxState;
    }
 
    public String getIdx() {
        return idx;
    }
 
    public void setIdx(String idx) {
        this.idx = idx;
    }
 
    public String getUserNM() {
        return userNM;
    }
 
    public void setUserNM(String userNM) {
        this.userNM = userNM;
    }
 
    public String getMobileNO() {
        return mobileNO;
    }
 
    public void setMobileNO(String mobileNO) {
        this.mobileNO = mobileNO;
    }
 
    public String getTelNO() {
        return telNO;
    }
 
    public void setTelNO(String telNO) {
        this.telNO = telNO;
    }
 
    public String getPhoto() {
        return photo;
    }
 
    public void setPhoto(String photo) {
        this.photo = photo;
    }
 
    public Boolean getCheckBoxState() {
        return checkBoxState;
    }
 
    public void setCheckBoxState(Boolean checkBoxState) {
        this.checkBoxState = checkBoxState;
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address_Item that = (Address_Item) o;
        return Objects.equals(idx, that.idx) && Objects.equals(userNM, that.userNM) && Objects.equals(mobileNO, that.mobileNO) && Objects.equals(telNO, that.telNO) && Objects.equals(photo, that.photo) && Objects.equals(checkBoxState, that.checkBoxState);
    }
 
    @Override
    public int hashCode() {
        return Objects.hash(idx, userNM, mobileNO, telNO, photo, checkBoxState);
    }
 
    public static DiffUtil.ItemCallback<Address_Item> itemCallback = new DiffUtil.ItemCallback<Address_Item>() {
        @Override
        public boolean areItemsTheSame(@NonNull Address_Item oldItem, @NonNull Address_Item newItem) {
            return oldItem.getIdx().equals(newItem.getIdx());
        }
 
        @Override
        public boolean areContentsTheSame(@NonNull Address_Item oldItem, @NonNull Address_Item newItem) {
            return oldItem.equals(newItem);
        }
    };
}
 

 

코드가 너무 긴 관계로 Java 에서 코틀린으로 코드를 변환시켰다.

그 다음에 var 변수 초기화 부분의 코드를 약간 수정했다.

이것은 Java 로 구현된 코드와 혼용 사용하는 Class 이다.

import android.os.Parcelable
import androidx.recyclerview.widget.DiffUtil
import kotlinx.parcelize.Parcelize
import java.util.*
 
@Parcelize
class Address_Item(
    // 결과를 받을 모델 (ArrayList 에 저장하므로 val 로 선언하면 안된다)
    // 서버 SQL의 칼럼명과 일치하게 작성해야 한다.
    var idx: String="",
    var userNM: String="",
    var mobileNO: String?=null,
    var telNO: String?=null,
    var photo: String?=null,
    var checkBoxState: Boolean
) : Parcelable {
    override fun equals(o: Any?): Boolean {
        if (this === o) return true
        if (o == null || javaClass != o.javaClass) return false
        val that = o as Address_Item
        return idx == that.idx && userNM == that.userNM && mobileNO == that.mobileNO && telNO == that.telNO && photo == that.photo && checkBoxState == that.checkBoxState
    }
 
    override fun hashCode(): Int {
        return Objects.hash(idx, userNM, mobileNO, telNO, photo, checkBoxState)
    }
 
    companion object {
        @JvmField
        var itemCallback: DiffUtil.ItemCallback<Address_Item> =
            object : DiffUtil.ItemCallback<Address_Item>() {
                override fun areItemsTheSame(oldItem: Address_Item, newItem: Address_Item): Boolean {
                    return oldItem.idx == newItem.idx
                }
 
                override fun areContentsTheSame(oldItem: Address_Item, newItem: Address_Item): Boolean {
                    return oldItem == newItem
                }
            }
    }
}

 

 

AddressActivity.java

- 기존 파일에서 객체를 생성하는 코드는 주석처리했다.

- Adpater 로 데이터를 전달하는 부분은 mAdpater.submitList(전달변수);

  이 코드는 서버에서 자료를 가져온 후 데이터를 넘기는 곳에 적어주면 된다.

   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);
    }

 

 

AddressListAdapter.java

- 기존 RecyclerView.Adapter 대신에 ListAdapter<T, VH> 를 상속한다.

- RecyclerView 가 화면에 표시할 데이터 타입 Address_Item 을 추가한다.

- 이렇게 변경하고 나면 생성자 부분이 에러가 발생한다. 자동으로 추가하면 protected 생성되는데 public 으로 변경하고 DiffUtil.ItemCallback 코드는 이곳에서 추가 구현해도 되고, Address_Item Class 에 추가 구현한 것을 사용해도 된다.

- rvItemList.get 대신에 getItem 으로 변경한다.

- getItemCount() 메소드는 삭제한다.

import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
 
// RecyclerView.Adapter 대신에 ListAdapter<T, VH> 를 상속한다.
// T는 리사이클러뷰가 화면에 표시할 데이터의 타입이다.
public class AddressListAdapter extends ListAdapter<Address_Item, AddressListAdapter.ViewHolder> {
    private final String TAG = this.getClass().getSimpleName();
    Context context;
 
    public AddressListAdapter(Context context, @NonNull DiffUtil.ItemCallback<Address_Item> diffCallback) {
        super(diffCallback);
        this.context = context;
    }
 
    @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){
        }
    }
 
}
 

 

 

 

블로그 이미지

Link2Me

,
728x90

Java 코드와 Kotlin 코드를 혼용하여 작성하는 환경 설정이 Android Studio 가 버전 업 되면서 변경되었다.

https://developer.android.com/kotlin/add-kotlin?hl=ko 이 URL을 참조하면 최신으로 변경된 것을 확인할 수 있다.

 

코틀린 최신 버전이 1.7.10 이지만 1.6.20 으로 설정했다.

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ext.kotlin_version = '1.6.20'
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
 
plugins {
    id 'com.android.application' version '7.2.2' apply false
    id 'com.android.library' version '7.2.2' apply false
}
 
 
task clean(type: Delete) {
    delete rootProject.buildDir
}

 

app build.gradle

plugins {
    id 'com.android.application'
    // 코틀린 혼용 사용을 위해 추가
    id 'kotlin-android'
    id 'kotlin-parcelize'
    id 'kotlin-kapt'
    // id 'kotlin-android-extensions' // deprecated 되었음.
}
 
android {
    compileSdk 32
 
    defaultConfig {
        applicationId "com.link2me.android.retrofit_mvvm_login"
        minSdk 23
        targetSdk 32
        versionCode 1
        versionName "1.1"
    }
 
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
 
    // View Binding : Android Studio 4.0 이상
    buildFeatures {
        viewBinding = true
    }
}
 
dependencies {
    // 아래 2줄은 코틀린 혼용 사용을 위해 추가
    implementation 'androidx.core:core-ktx:1.8.0' // 2022년 6월 1일 버전
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
 
    implementation 'androidx.appcompat:appcompat:1.4.2'
    implementation 'com.google.android.material:material:1.6.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
 
//    implementation 'gun0912.ted:tedpermission:2.0.0'  // 위험권한 퍼미션 처리
    implementation 'io.github.ParkSangGwon:tedpermission-normal:3.3.0' // 위험권한 퍼미션 처리
    implementation 'androidx.cardview:cardview:1.0.0'  // 레이아웃으로 사용할 CardView
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
 
    implementation 'androidx.navigation:navigation-fragment:2.1.0'
    implementation 'androidx.navigation:navigation-ui:2.1.0'
 
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
    implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0'
 
    def lifecycle_version = "2.5.0"
    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
    // lifecycle-extensions의 API는 지원 중단되었다.
 
    // 이미지 출력용 Glide
    implementation 'com.github.bumptech.glide:glide:4.11.0'
    //annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
    kapt 'com.github.bumptech.glide:compiler:4.11.0' // Kotlin plugin doesn't pick up annotationProcessor dependencies
}

 

 

 

 

블로그 이미지

Link2Me

,
728x90

Retrofit 라이브러리를 사용하면 Session 처리 OK이고, 코드가 심플한 점도 장점이다.

MVVM 적용한 코드로 된 강좌들을 접하다보니 안할 수가 없어 기존 코드를 MVVM 코드로 정리를 해 볼 필요성이 있어 코드를 수정하여 구현해 보고 나서 동작에 대한 개념이 이해되었다.

 

디자인 패턴은 최고의 개발자들이 만들어낸 솔루션이다. 또한, 오랫동안 사용되어 오면서 안정성 검증이 완료된 솔루션으로 심지어 디자인 패턴은 문서화도 잘 되어있다.

 

MVVM 패턴
MVVM 패턴은 마틴 파울러의 Presentation 모델 패턴에서 파생된 디자인 패턴이다. 
MVVM 패턴의 목표는 비즈니스 로직과 프레젠테이션 로직을 UI로부터 분리하는 것이다. 
비즈니스 로직과 프레젠테이션 로직을 UI로부터 분리하게 되면, 테스트, 유지 보수, 재사용이 쉬워진다. 

- Model : 어플리케이션에서 사용되는 데이터와 그 데이터를 처리하는 부분.
- View : 사용자에서 보여지는 UI 부분.
- View Model : View를 표현하기 위해 만들어진 View를 위한 Model.

  View와는 Binding을 하여 연결후 View에게서 액션을 받고 또한 View를 업데이트한다.

 

1. View에 입력이 들어오면 ViewModel에게 명령을 한다.
2. ViewModel은 필요한 데이터를 Model에게 요청한다.
3. Model은 ViewModel에게 요청된 데이터를 응답한다.
4. ViewModel은 응답 받은 데이터를 가공해서 저장한다.
5. View는 ViewModel과의 Data Binding으로 인해 자동으로 갱신된다.

 

 

Retrofit 라이브러리를 활용한 MVVM 적용 Login 샘플 코드 예제이다.

 

public class LoginActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();
    Context mContext;
    private ActivityLoginBinding binding;
 
    private BackPressHandler backPressHandler;
    private AuthViewModel viewModel;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_login);
        binding = ActivityLoginBinding.inflate(getLayoutInflater());
        View view = binding.getRoot();
        setContentView(view);
 
        backPressHandler = new BackPressHandler(this);
        mContext = LoginActivity.this;
 
        Utils.viewShowHide(binding.btnLogin, false);
        initView();
    }
 
    private void initView() {
        // viewModel init
        viewModel = new ViewModelProvider(this).get(AuthViewModel.class);
 
        // viewModel observe
        viewModel.getLoginResultLiveData().observe(thisnew Observer<LoginResult>() {
            @Override
            public void onChanged(LoginResult loginResult) {
                if(loginResult.getStatus().contains("success")){
                    Utils.goNextActivity(mContext,MainActivity.class);
                    finish();
                } else {
                    Utils.showAlert(mContext, loginResult.getStatus(), loginResult.getMessage());
                }
            }
        });
 
        // addTextChangedListener
        binding.etPw.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
 
            }
 
            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
 
            }
 
            @Override
            public void afterTextChanged(Editable editable) {
                String userID = binding.etId.getText().toString().trim();
                String password = binding.etPw.getText().toString().trim();
                if (!userID.isEmpty() && !password.isEmpty())
                    Utils.viewShowHide(binding.btnLogin, true);
                // userID 와 패스워드 모두 입력하면 로그인 버튼이 활성화된다.
            }
        });
 
        // button setOnClickListener
        binding.btnLogin.setOnClickListener(view -> {
            login();
        });
 
    }
 
    private void login() {
        String userID = binding.etId.getText().toString().trim();
        String password = binding.etPw.getText().toString().trim();
        String uID = Utils.getDeviceId(mContext); // 스마트폰 고유장치번호
        String mfoneNO = Utils.getPhoneNumber(mContext); // 스마트폰 전화번호
        viewModel.login(userID, password,uID,mfoneNO);
        /*
        로그인 전달 변수 수정 시
        AutoViewModel.java 와 AutoRepository.java 파일 모두 수정
        */
    }
 
 
    @Override
    public void onBackPressed() {
        //super.onBackPressed();
        backPressHandler.onBackPressed();
    }
}

 

 

public class AuthViewModel extends ViewModel {
    private AuthRepository repository;
    private LiveData<LoginResult> _liveData;
 
    public AuthViewModel() {
    }
 
    public void login(String userID, String password, String uID, String mfoneNO) {
        repository.loginRepo(userID, password, uID, mfoneNO);
    }
 
    public LiveData<LoginResult> getLoginResultLiveData() {
        if (_liveData == null) {
            repository = new AuthRepository();
            _liveData = repository.getLoginResultLiveData();
        }
        return _liveData;
    }
 
}
 

 

 

 

public class AuthRepository {
    private RetrofitService loginService;
    private MutableLiveData<LoginResult> loginResultLiveData;
 
    public AuthRepository() {
        loginResultLiveData = new MutableLiveData<>();
        loginService = RetrofitAdapter.getClient(RetrofitURL.BaseUrl).create(RetrofitService.class);
    }
 
    public void loginRepo(String userID, String password, String uID, String mfoneNO){
        loginService.Login(userID,password,uID,mfoneNO)
                .enqueue(new Callback<LoginResult>() {
                    @Override
                    public void onResponse(Call<LoginResult> call, Response<LoginResult> response) {
                        loginResultLiveData.postValue(response.body());
                    }
 
                    @Override
                    public void onFailure(Call<LoginResult> call, Throwable t) {
                        loginResultLiveData.postValue(null);
                    }
                });
    }
 
    public LiveData<LoginResult> getLoginResultLiveData() {
        return loginResultLiveData;
    }
}

 

MVVM 기반 코드는 위 3개의 파일이 핵심인 거 같다.

전체 소스 코드는 Github 에 올려 두었다.

https://github.com/jsk005/JavaProjects/tree/master/retrofit_mvvm_login

 

GitHub - jsk005/JavaProjects: 자바 기능 테스트

자바 기능 테스트. Contribute to jsk005/JavaProjects development by creating an account on GitHub.

github.com

 

 

아래 GitHub를 참고하면 유용한 자료들이 많다.

https://github.com/probelalkhan?tab=repositories 

 

probelalkhan - Overview

probelalkhan has 147 repositories available. Follow their code on GitHub.

github.com

 

블로그 이미지

Link2Me

,
728x90

RxAndroid and Retrofit 를 활용하여 파일을 다운로드하고 PDF Viewer 처리하는 코드 예제이다.

 

build.gradle:

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // 코틀린 혼용 사용을 위해 추가
 
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
    implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0'
 
    // ReactiveX : Observable에서 보낸 이벤트를 Operator에서 가공하고 Subscribe에서 받는 것
    implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
    implementation 'io.reactivex.rxjava3:rxjava:3.1.5'
    implementation "com.github.akarnokd:rxjava3-retrofit-adapter:3.0.0"
 
}

 

RetrofitService.java:

import io.reactivex.rxjava3.core.Observable;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Streaming;
import retrofit2.http.Url;
 
public interface RetrofitService {
    // POST 방식으로 데이터를 주고 받을 때 넘기는 변수는 Field 라고 해야 한다.
 
    @FormUrlEncoded
    @POST(RetrofitUrl.PdfData)
    Call<PdfResult> getPdfData(
            @Field("keyword"String keyword,
            @Field("search"String search
    );
 
    @Streaming
    @GET
    Observable<Response<ResponseBody>> downloadFile(@Url String fileUrl);
 
}
 

 

RetrofitAdapter.java:

import hu.akarnokd.rxjava3.retrofit.RxJava3CallAdapterFactory;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
 
public class RetrofitAdapter {
    static Retrofit retrofit = null;
    static Retrofit retrofit_service = null;
 
    public static Retrofit getClient() {
        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(RetrofitUrl.BASE_URL)
                    .client(createOkHttpClient())
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }
 
    public static OkHttpClient createOkHttpClient() {
        // 네트워크 통신 로그(서버로 보내는 파라미터 및 받는 파라미터) 보기
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        //builder.cookieJar(new RetrofitCookieJar()); // Session 통신을 위한 기능
        // 네트워크 통신 로그(서버로 보내는 파라미터 및 받는 파라미터) 보기
        HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        builder.addInterceptor(httpLoggingInterceptor);
        builder.connectTimeout(15, TimeUnit.SECONDS);
        builder.readTimeout(20, TimeUnit.SECONDS);
        builder.writeTimeout(20, TimeUnit.SECONDS);
        builder.retryOnConnectionFailure(true);
        return builder.build();
    }
 
    public static <T> T createService(Class<T> serviceClass) {
        // implementation "com.github.akarnokd:rxjava3-retrofit-adapter:3.0.0"
        if (retrofit_service == null) {
            retrofit_service = new Retrofit.Builder()
                    .baseUrl(RetrofitUrl.BASE_URL)
                    .client(createOkHttpClient())
                    .addCallAdapterFactory(RxJava3CallAdapterFactory.create()).build();
        }
        return retrofit_service.create(serviceClass);
    }
 
}
 

 

RetrofitUrl.java:

public class RetrofitUrl {
    public static final String BASE_URL = "http://www.abc.com/";
 
    public static final String PdfData = "androidSample/getPDFData.php";
}

 

 

MainActivity.java:

import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.annotations.NonNull;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.ObservableEmitter;
import io.reactivex.rxjava3.core.ObservableOnSubscribe;
import io.reactivex.rxjava3.core.Observer;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers;
import okhttp3.ResponseBody;
import okio.BufferedSink;
import okio.Okio;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
 
public class MainActivity extends AppCompatActivity implements BindPdfViewListAdapter.OnItemClickListener {
    private final String TAG = this.getClass().getSimpleName();
    Context mContext;
    
    @Override
    public void onItemClicked(View view, Pdf_Item item, int position) {
        Toast.makeText(getApplicationContext(), "잠시 기다리시면 "+item.getTitle()+" PDF 파일 열람이 가능합니다", Toast.LENGTH_LONG).show();
        String PDFUrl = RetrofitUrl.BASE_URL+item.getPdfurl();
        downloadPDFFile(item.getPdfurl());
    }
 
    public void downloadPDFFile(String fileUrl) {
        RetrofitService downloadService = RetrofitAdapter.createService(RetrofitService.class);
        downloadService.downloadFile(fileUrl)
                .flatMap(processResponse())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(handleResult());
    }
 
    public Function<Response<ResponseBody>, Observable<File>> processResponse(){
        return new Function<Response<ResponseBody>, Observable<File>>() {
            @Override
            public Observable<File> apply(Response<ResponseBody> responseBodyResponse) throws Exception {
                return saveToDiskRx(responseBodyResponse);
            }
        };
    }
 
    private Observable<File> saveToDiskRx(final Response<ResponseBody> response){
        return Observable.create(new ObservableOnSubscribe<File>() {
            @Override
            public void subscribe(@NonNull ObservableEmitter<File> subscriber) throws Throwable {
                File filePath = new File(Environment.getExternalStorageDirectory() + "/download");
                File outputFile = new File(filePath, "tempPDF.pdf");
                if (outputFile.exists()) { // 기존 파일 존재시 삭제하고 다운로드
                    outputFile.delete();
                }
 
                BufferedSink bufferedSink = Okio.buffer(Okio.sink(outputFile));
                bufferedSink.writeAll(response.body().source());
                bufferedSink.close();
 
                subscriber.onNext(outputFile);
                subscriber.onComplete();
            }
        });
    }
 
    private Observer<File> handleResult() {
        return new Observer<File>() {
 
            @Override
            public void onSubscribe(@NonNull Disposable d) {
                Log.d(TAG, "OnSubscribe");
            }
 
            @Override
            public void onNext(@NonNull File file) {
                Log.d(TAG, "File downloaded to " + file.getAbsolutePath());
                // 다운로드한 파일 실행하여 PDF 보기 진행하는 코드
                if (Build.VERSION.SDK_INT >= 24) {
                    openPDF(file);
                } else {
                    Intent intent = new Intent(Intent.ACTION_VIEW);
                    Uri apkUri = Uri.fromFile(file);
                    intent.setDataAndType(apkUri, "application/pdf");
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    getApplicationContext().startActivity(intent);
                }
            }
 
            @Override
            public void onError(@NonNull Throwable e) {
                e.printStackTrace();
                Log.d(TAG, "Error " + e.getMessage());
            }
 
            @Override
            public void onComplete() {
                Log.d(TAG, "onCompleted");
            }
        };
    }
 
    void openPDF(File file) {
        Uri fileUri = FileProvider.getUriForFile(mContext, mContext.getApplicationContext().getPackageName() + ".fileprovider",file);
 
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(fileUri, "application/pdf");
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        try {
            startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
 

 

전체 소스코드

https://github.com/jsk005/JavaProjects/tree/master/pdfviewer

 

GitHub - jsk005/JavaProjects: 자바 기능 테스트

자바 기능 테스트. Contribute to jsk005/JavaProjects development by creating an account on GitHub.

github.com

 

참고자료

https://androiderrors.com/rxandroid-and-retrofit-unable-to-create-call-adapter-for-io-reactivex-observableretrofit2-responseokhttp3-responsebody/

https://www.codexpedia.com/android/retrofit-2-and-rxjava-for-file-downloading-in-android/

 

이 자료는 참고하지 않았지만 나중에 보면 도움되는 것이 있을 수도 있겠다 싶어 URL를 적어둔다.

https://www.digitalocean.com/community/tutorials/android-rxjava-retrofit

 

 

 

 

 

 

 

블로그 이미지

Link2Me

,
728x90

SQLite DB의 테이블 데이터를 가져오는 쿼리 작성시 약간 잘못 생각하면 엉뚱한 결과가 나오더라.

public ArrayList<String> getAllCateData(String category){
    ArrayList<String> sqliteDBData = new ArrayList<>();
    // Select All Query
    String selectQuery = "SELECT * FROM " + HISTORY_TABLE + " WHERE category= '" + category + "' ORDER BY datetime ASC";
    SQLiteDatabase db = mHelper.getWritableDatabase();
    Cursor cursor = db.rawQuery(selectQuery, null);
 
    System.out.println("cursor.getCount : "+cursor.getCount());
    System.out.println("selectQuery : "+ selectQuery);
 
    if(cursor != null)
        if(cursor.moveToFirst()){ // 반드시 이런 순서로 해야 문제가 없더라.
            do {
                // ArrayList에 추가
                int column_index = cursor.getColumnIndex(Column_history);
                sqliteDBData.add(cursor.getString(column_index));
            } while (cursor.moveToNext());
        }
    cursor.close();
    return sqliteDBData;
}
 

 

 전체 소스는 GitHub 에 올려진 코드를 참고하면 된다.

https://github.com/jsk005/JavaProjects/tree/master/sqlitedb

블로그 이미지

Link2Me

,
728x90

https://link2me.tistory.com/1506 에 있는 파일 다운로드 및 실행 코드와 원리는 동일한 것도 포함되어 있으니 참고하면 도움된다.

 

전체 소스코드는 GitHUB 에 올려진 것을 참조하시라.

서버 코드와 관련된 URL, AES Key는 변경해서 올렸다.

https://github.com/jsk005/JavaProjects/tree/master/pdfviewer

 

GitHub - jsk005/JavaProjects: 자바 기능 테스트

자바 기능 테스트. Contribute to jsk005/JavaProjects development by creating an account on GitHub.

github.com

 

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.link2me.android.pdfviewer">
 
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission
        android:name="android.permission.REQUEST_INSTALL_PACKAGES"
        tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:usesCleartextTraffic="true">
        <activity
            android:name=".ui.MainActivity"
            android:exported="false" />
        <activity
            android:name=".ui.SplashActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
 
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"
                tools:replace="android:resource" />
        </provider>
    </application>
 
</manifest>
 

 

 

 

package com.link2me.android.pdfviewer.ui;
 
import android.content.Context;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.Toast;
 
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
 
import com.link2me.android.common.BackPressHandler;
import com.link2me.android.common.Utils;
import com.link2me.android.common.Value;
import com.link2me.android.pdfviewer.R;
import com.link2me.android.pdfviewer.adapter.BindPdfViewListAdapter;
import com.link2me.android.pdfviewer.databinding.ActivityMainBinding;
import com.link2me.android.pdfviewer.model.PdfResult;
import com.link2me.android.pdfviewer.model.Pdf_Item;
import com.link2me.android.pdfviewer.network.RetrofitAdapter;
import com.link2me.android.pdfviewer.network.RetrofitService;
import com.link2me.android.pdfviewer.network.RetrofitUrl;
 
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
 
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
 
public class MainActivity extends AppCompatActivity implements BindPdfViewListAdapter.OnItemClickListener {
    private final String TAG = this.getClass().getSimpleName();
    Context mContext;
 
    private ActivityMainBinding binding;
 
    private ArrayList<Pdf_Item> pdfItemList = new ArrayList<>();
    private BindPdfViewListAdapter mAdapter;
 
    DownloadPdfFromURL downloadApk;
    private File outputFile;
 
    private BackPressHandler backPressHandler;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        View view = binding.getRoot();
        setContentView(view);
 
        backPressHandler = new BackPressHandler(this); // 뒤로 가기 버튼 이벤트
        mContext = MainActivity.this;
        initView();
    }
 
    private void initView() {
        createPdfList(); // 서버 데이터 가져오기
        buildRecyclerView();
    }
 
    private void createPdfList() {
        String keyword = Value.encryptAES(Value.URLkey());
        RetrofitService service = RetrofitAdapter.getClient().create(RetrofitService.class);
        Call<PdfResult> call = service.getPdfData(keyword,"");
        call.enqueue(new Callback<PdfResult>() {
            @Override
            public void onResponse(Call<PdfResult> call, Response<PdfResult> response) {
                if(response.body().getStatus().contains("success")){
                    pdfItemList.clear(); // 서버에서 가져온 데이터 초기화
 
                    for(Pdf_Item item: response.body().getPdfinfo()){
                        pdfItemList.add(item);
                    }
 
                    // runOnUiThread()를 호출하여 실시간 갱신한다.
                    runOnUiThread(() -> {
                        // 갱신된 데이터 내역을 어댑터에 알려줌
                        mAdapter.notifyDataSetChanged();
                    });
                } else {
                    Utils.showAlert(mContext,response.body().getStatus(),response.body().getMessage());
                }
            }
 
            @Override
            public void onFailure(Call<PdfResult> call, Throwable t) {
 
            }
        });
    }
 
    private void buildRecyclerView(){
        binding.pdfListview.setHasFixedSize(true);
        LinearLayoutManager manager = new LinearLayoutManager(mContext);
        mAdapter = new BindPdfViewListAdapter(mContext,pdfItemList); // 객체 생성
 
        DividerItemDecoration decoration = new DividerItemDecoration(mContext,manager.getOrientation());
        binding.pdfListview.addItemDecoration(decoration);
        binding.pdfListview.setLayoutManager(manager);
        binding.pdfListview.setAdapter(mAdapter);
 
        mAdapter.setOnItemSelectClickListener(this);
    }
 
    @Override
    public void onItemClicked(View view, Pdf_Item item, int position) {
//        Log.d(TAG, RetrofitUrl.BASE_URL+item.getPdfurl());
        Toast.makeText(getApplicationContext(), "잠시 기다리시면 "+item.getTitle()+" PDF 파일 열람이 가능합니다", Toast.LENGTH_LONG).show();
        String PDFUrl = RetrofitUrl.BASE_URL+item.getPdfurl();
        downloadPDF(PDFUrl);
    }
 
    private void downloadPDF(String fileUrl) {
        // 백그라운드 객체를 만들어 주어야 다운로드 취소가 제대로 동작됨
        downloadApk = new DownloadPdfFromURL();
        downloadApk.execute(fileUrl);
    }
 
    class DownloadPdfFromURL extends AsyncTask<String, Integer, String> {
 
        @Override
        protected String doInBackground(String... strings) {
            int count;
            int lenghtOfFile = 0;
            InputStream input = null;
            OutputStream fos = null;
 
            File filePath = new File(Environment.getExternalStorageDirectory() + "/download");
            outputFile = new File(filePath, "tempPDF.pdf");
            if (outputFile.exists()) { // 기존 파일 존재시 삭제하고 다운로드
                outputFile.delete();
            }
 
            try {
                URL url = new URL(strings[0]);
                URLConnection connection = url.openConnection();
                connection.connect();
 
                lenghtOfFile = connection.getContentLength(); // 파일 크기를 가져옴
 
                input = new BufferedInputStream(url.openStream());
                fos = new FileOutputStream(outputFile);
                byte data[] = new byte[1024];
                long total = 0;
 
                while ((count = input.read(data)) != -1) {
                    if (isCancelled()) {
                        input.close();
                    }
                    total = total + count;
                    if (lenghtOfFile > 0) { // 파일 총 크기가 0 보다 크면
                        publishProgress((int) (total * 100 / lenghtOfFile));
                    }
                    fos.write(data, 0, count); // 파일에 데이터를 기록
                }
 
                fos.flush();
 
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (input != null) {
                    try {
                        input.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return null;
        }
 
        protected void onPostExecute(String result) {
            if (result == null) {
                // 미디어 스캐닝
                MediaScannerConnection.scanFile(getApplicationContext(), new String[]{outputFile.getAbsolutePath()}, nullnew MediaScannerConnection.OnScanCompletedListener() {
                    @Override
                    public void onScanCompleted(String s, Uri uri) {
 
                    }
                });
 
                // 다운로드한 파일 실행하여 업그레이드 진행하는 코드
                if (Build.VERSION.SDK_INT >= 24) {
                    openPDF(outputFile);
                } else {
                    Intent intent = new Intent(Intent.ACTION_VIEW);
                    Uri apkUri = Uri.fromFile(outputFile);
                    intent.setDataAndType(apkUri, "application/pdf");
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    getApplicationContext().startActivity(intent);
                }
 
            } else {
                Toast.makeText(getApplicationContext(), "다운로드 에러", Toast.LENGTH_LONG).show();
            }
        }
    }
 
    void openPDF(File file) {
        Uri fileUri = FileProvider.getUriForFile(mContext, mContext.getApplicationContext().getPackageName() + ".fileprovider",file);
 
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(fileUri, "application/pdf");
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        try {
            startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    @Override
    public void onBackPressed() {
        backPressHandler.onBackPressed();
    }
}
 

 

 

'안드로이드 > Android 활용' 카테고리의 다른 글

Android APP(Java)에서 PHP Session 저장 및 사용  (0) 2023.12.23
WebView assets html 파일 읽기  (0) 2020.11.27
Android TextToSpeech  (0) 2020.09.30
Intent 이메일 전송하기  (0) 2020.09.08
Profile 이미지 처리  (0) 2020.09.01
블로그 이미지

Link2Me

,
728x90

RecyclerView가 보여주는 ITEM VIEW 들을 만들어주는 adapter에서 사용하는 ViewHolder의 아이템뷰에 view binding을 적용하는 것이다.

 

테스트를 하다보니 RecyclerView 를 적용한 Adapter 와 Activity 모두 view binding 을 적용해야 이미지가 정상적으로 보이는 것을 확인할 수 있었다.

 

변경되는 사항

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        // 새로운 뷰를 만든다.
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.address_item,parent,false);
        ViewHolder viewHolder = new ViewHolder(itemView);
        return viewHolder;
    }
 
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        AddressItemBinding binding = AddressItemBinding.inflate(LayoutInflater.from(parent.getContext()),parent,false);
        return new ViewHolder(binding);
    }

 

 

    public class ViewHolder extends RecyclerView.ViewHolder  {
        View layout;
        ImageView photoImg;
        TextView tv_name;
        TextView tv_mobileNO;
        TextView tv_telNO;
        ImageView call_btn;
        CheckBox cbSelect;
 
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            // 화면에 표시될 View 로부터 위젯에 대한 참조 획득
            layout = itemView.findViewById(R.id.child_layout);
            photoImg = itemView.findViewById(R.id.profile_Image);
            tv_name = itemView.findViewById(R.id.child_name);
            tv_mobileNO = itemView.findViewById(R.id.child_mobileNO);
            tv_telNO = itemView.findViewById(R.id.child_telNO);
            call_btn = itemView.findViewById(R.id.child_Btn);
            cbSelect = itemView.findViewById(R.id.list_cell_checkbox);
        }
    }
 
    public class ViewHolder extends RecyclerView.ViewHolder  {
        AddressItemBinding itemBinding;
 
        public ViewHolder(@NonNull AddressItemBinding binding) {
            super(binding.getRoot());
            itemBinding = binding;
        }
 
        void bindItem(Address_Item item){
            if(!TextUtils.isEmpty(item.getPhoto())){
                Glide.with(mContext).load(item.getPhoto()).into(itemBinding.profileImage);
            }
            itemBinding.childName.setText(item.getUserNM());
            itemBinding.childMobileNO.setText(PhoneNumberUtils.formatNumber(item.getMobileNO()));
            itemBinding.childTelNO.setText(PhoneNumberUtils.formatNumber(item.getTelNO()));
 
            itemBinding.childBtn.setOnClickListener(v -> {
 
            });
            itemBinding.listCellCheckbox.setChecked(item.getCheckBoxState());
        }
    }
 

 

View Binding을 적용한 코드

 

package com.link2me.android.sample.adapter;
 
import static com.link2me.android.sample.main.AddrBindActivity.constraintLayout;
import static com.link2me.android.sample.main.AddrBindActivity.isCheckFlag;
 
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
 
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
 
import com.bumptech.glide.Glide;
import com.link2me.android.sample.R;
import com.link2me.android.sample.databinding.AddressItemBinding;
import com.link2me.android.sample.model.Address_Item;
import com.link2me.android.sample.network.RetrofitUrl;
 
import java.util.ArrayList;
 
public class BindListViewAdapter extends RecyclerView.Adapter<BindListViewAdapter.ViewHolder> {
    private final String TAG = this.getClass().getSimpleName();
    Context context;
    private ArrayList<Address_Item> rvItemList;
 
    // 인터페이스 선언 -------------------------------------------------------------
    private OnItemClickListener mListener;
 
    public interface OnItemClickListener {
        void onItemClicked(View view, Address_Item item, int position);
    }
 
    public void setOnItemSelectClickListener(OnItemClickListener listener){
        mListener = listener;
    }
    // 인터페이스 ----------------------------------------------------------------
 
    public BindListViewAdapter(Context context, ArrayList<Address_Item> items) {
        this.context = context;
        rvItemList = items;
    }
 
    public void selectAll(){ // checkbox 전체 선택
        for(int i=0;i < rvItemList.size();i++){
            rvItemList.get(i).setCheckBoxState(true);
        }
        notifyDataSetChanged();
    }
    public void unselectall(){ // checkbox 전체 해제
        for(int i=0;i < rvItemList.size();i++){
            rvItemList.get(i).setCheckBoxState(false);
        }
        notifyDataSetChanged();
    }
 
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        // 새로운 뷰를 만든다.
        AddressItemBinding binding = AddressItemBinding.inflate(LayoutInflater.from(parent.getContext()),parent,false);
        return new ViewHolder(binding);
    }
 
    @Override
    public void onBindViewHolder(@NonNull BindListViewAdapter.ViewHolder holder, int position) {
        holder.bindItem(rvItemList.get(position), position);
    }
 
    @Override
    public int getItemCount() {
        return rvItemList.size();
    }
 
    public class ViewHolder extends RecyclerView.ViewHolder  {
        AddressItemBinding itemBinding;
 
        public ViewHolder(@NonNull AddressItemBinding binding) {
            super(binding.getRoot());
            itemBinding = binding;
        }
 
        void bindItem(Address_Item item, int position){
            String photoURL = RetrofitUrl.BASE_URL + "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(view1,item,position);
                    // 클릭의 결과로 값을 전달
                    Log.e(TAG,"item clicked : "+position);
                }
            });
 
            final String[] items ={"휴대폰 전화걸기","사무실전화 걸기"};
            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
            builder.setTitle("해당작업을 선택하세요");
            builder.setItems(items, (dialog, which) -> {
                Toast.makeText(context, items[which] + "선택했습니다.", Toast.LENGTH_SHORT).show();
                switch (which){
                    case 0:
                        if(item.getMobileNO().length() ==0){
                            Toast.makeText(context, "전화걸 휴대폰 번호가 없습니다.",Toast.LENGTH_SHORT).show();
                            break;
                        }
 
                        AlertDialog dialog1 = new AlertDialog.Builder(context)
                                .setTitle(item.getUserNM())
                                .setMessage(PhoneNumberUtils.formatNumber(item.getMobileNO()) + " 통화하시겠습니까?")
                                .setPositiveButton("예",
                                        (dialog23, which13) -> {
 
                                            Intent intent = new Intent(Intent.ACTION_CALL,
                                                    Uri.parse("tel:" + PhoneNumberUtils.formatNumber(item.getMobileNO())));
                                            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                                            context.startActivity(intent);
                                        })
                                .setNegativeButton(
                                        "아니오",
                                        (dialog22, which12) -> dialog22.dismiss()).create();
                        dialog1.show();
                        break;
                    case 1:
                        if(item.getTelNO().length() ==0){
                            Toast.makeText(context, "전화걸 사무실 번호가 없습니다.",Toast.LENGTH_SHORT).show();
                            break;
                        }
                        AlertDialog dialog2 = new AlertDialog.Builder(context)
                                .setTitle(item.getUserNM())
                                .setMessage(PhoneNumberUtils.formatNumber(item.getTelNO()) + " 통화하시겠습니까?")
                                .setPositiveButton("예",
                                        (dialog3, which1) -> {
 
                                            Intent intent = new Intent(Intent.ACTION_CALL,
                                                    Uri.parse("tel:" + PhoneNumberUtils.formatNumber(item.getTelNO())));
                                            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                                            context.startActivity(intent);
                                        })
                                .setNegativeButton(
                                        "아니오",
                                        (dialog32, which14) -> dialog32.dismiss()).create();
                        dialog2.show();
                        break;
 
                }
            });
            builder.create();
 
            itemBinding.callBtn.setOnClickListener(v -> builder.show());
 
 
            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, "상세보기를 눌렀습니다 ===" + 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(rvItemList.get(position).getCheckBoxState() == true){
                        rvItemList.get(position).setCheckBoxState(false);
                        Log.d("checkbox","position : "+ position + " checkBoxState === "+ rvItemList.get(position).getCheckBoxState());
                    } else {
                        rvItemList.get(position).setCheckBoxState(true);
                        Log.d("checkbox","position : "+ position + " checkBoxState === "+ rvItemList.get(position).getCheckBoxState());
                    }
                });
 
                itemBinding.childLayout.setOnClickListener(v -> {
                    if(itemBinding.listcellCheckbox.isChecked() == false){
                        itemBinding.listcellCheckbox.setChecked(true);
                        rvItemList.get(position).setCheckBoxState(true);
                        //notifyDataSetChanged();
                        Log.d("checklist","position : "+ position + " checkBoxState === "+ rvItemList.get(position).getCheckBoxState());
                    } else {
                        itemBinding.listcellCheckbox.setChecked(false);
                        rvItemList.get(position).setCheckBoxState(false);
                        //notifyDataSetChanged();
                        Log.d("checklist","position : "+ position + " checkBoxState === "+ rvItemList.get(position).getCheckBoxState());
                    }
                });
            }
 
            // 재사용 문제 해결
            if(rvItemList.get(position).getCheckBoxState() == true){
                itemBinding.listcellCheckbox.setChecked(true);
                Log.d("ReUse","position : " + position + " checkBoxState === " + rvItemList.get(position).getCheckBoxState());
            } else {
                itemBinding.listcellCheckbox.setChecked(false);
                Log.d("ReUse","position : "+position + " checkBoxState ===" + rvItemList.get(position).getCheckBoxState());
            }
        }
    }
 
}
 
 
 
 
 

 

 

 

 

블로그 이미지

Link2Me

,
728x90

세부사항은 https://developer.android.com/about/versions/12/behavior-changes-12?hl=ko

 

동작 변경사항: Android 12를 타겟팅하는 앱  |  Android Developers

모든 앱에 영향을 주는 Android 12의 변경사항을 알아봅니다.

developer.android.com

를 읽어보면 된다.

 

앱이 Android 12 이상을 타겟팅하고 인텐트 필터를 사용하는 활동이나 서비스, broadcast receiver를 포함하면 이러한 앱 구성요소의 android:exported 속성을 명시적으로 선언해야 한다.

<service android:name="com.example.app.backgroundService"
         android:exported="false">
    <intent-filter>
        <action android:name="com.example.app.START_BACKGROUND" />
    </intent-filter>
</service>

 

android:exported
- 앱의 활동에 인텐트 필터가 포함되면 다른 앱에서 Activity를 시작할 수 있도록 이 요소를 'true'로 설정해야 한다.
- 모든 앱에서 Activity에 액세스할 수 있으며 정확한 클래스 이름으로 활동을 시작할 수 있다.
- 인텐트 필터가 없는 경우의 기본값은 false 이다.
- 같은 애플리케이션의 구성요소나 사용자 ID가 같은 애플리케이션, 권한있는 시스템 구성요소에서만 시작될 수 있다.

 

앱 build.gradle

apply plugin: 'com.android.application'
 
android {
    compileSdkVersion 31
    defaultConfig {
        applicationId "com.spectrum.android.ping"
        minSdkVersion 23
        targetSdkVersion 31
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
 
dependencies {
    implementation project(':lib')
}

 

오래된 앱을 targetSdkVersion 31 로 업데이트하면 아래와 같은 에러가 발생한다.

 

아래와 같이 수정되면 OK

Android 10 이라도 targetSdkVersion 31로 하면 android:exported="true" 설정해야 동작된다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.spectrum.android.ping">
 
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        >
        <activity android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>
 

 

블로그 이미지

Link2Me

,
728x90

박상권님이 GitHub 에 오픈해 주신 https://github.com/ParkSangGwon/TedNaverMapClustering 

 

GitHub - ParkSangGwon/TedNaverMapClustering: 네이버지도용 클러스터 유틸리티 라이브러리

네이버지도용 클러스터 유틸리티 라이브러리. Contribute to ParkSangGwon/TedNaverMapClustering development by creating an account on GitHub.

github.com

Android 용 TedNaverMapClustering 파일을 받아서 구현한 네이버 지도에 Clustering 기능을 구현했다.

 

라이브러리 적용 전 코드와 적용 후 코드를 구분하여 적어둔다.

서버로부터 받아오는 데이터 구조가 아래와 같이 구현되어 있었다. (일부 변수는 삭제)

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
 
@Parcelize
data class ItemData (
    val code: String,
    val bldNM: String// 빌딩명
    val address: String// 주소
    var latitude: Double, // 위도
    var longitude: Double, // 경도
): Parcelable

 

TedNaverMapClustering 적용 후 dataclass

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
 
@Parcelize
data class ItemData (
    val code: String,
    val bldNM: String// 빌딩명
    val address: String// 주소
    var latitude: Double, // 위도
    var longitude: Double, // 경도
): Parcelable
 

 

ItemData data class 는 그대로 두고 별도로 ItemData 에서 필수 항목만 뽑아서 아래와 같이 코드를 작성한다.

import ted.gun0912.clustering.clustering.TedClusterItem
import ted.gun0912.clustering.geometry.TedLatLng

data class NaverItem(var position: LatLng) : TedClusterItem {
    override fun getTedLatLng(): TedLatLng {
        return TedLatLng(position.latitude, position.longitude)
    }
 
    var code: String= null
    var bldNM: String= null
    var chtype: Int = 0
    var Cust: Int = 0
 
    constructor(lat: Double, lng: Double) : this(LatLng(lat, lng)) {
        code = null
        bldNM = null
        chtype = 0
        Cust = 0
    }
 
    constructor(lat: Double, lng: Double, code: String?, bldNM: String?, chtype: Int, Cust: Int) : this(
        LatLng(lat, lng)
    ) {
        this.code = code
        this.bldNM = bldNM
        this.chtype = chtype
        this.Cust = Cust
    }
}

 

 

기존 marker 처리 코드

https://navermaps.github.io/android-map-sdk/guide-ko/5-1.html 부분에 "대량의 오버레이를 다룰 경우 객체를 생성하고 초기 옵션을 지정하는 작업은 백그라운드 쓰레드에서 수행하고 지도에 추가하는 작업만을 메인 쓰레드에서 수행하면 메인 쓰레드를 효율적으로 사용할 수 있습니다." 라고 나온다. 많은 marker 를 생성시에는 백그라운드 쓰레드에서 처리하도록 하고 있다.

var activeMarkers: MutableList<Marker>= null
private fun makeAllMarker(itemList: List<ItemData>) {
    val executor = Executors.newSingleThreadExecutor()
    val handler = Handler(Looper.getMainLooper())
    executor.execute {
        // 백그라운드 쓰레드
        activeMarkers = ArrayList()
        for (i in itemList.indices) {
            val itemData = itemList[i]
            val marker = Marker()
            marker.position = LatLng(itemData.latitude, itemData.longitude)
            marker.icon = OverlayImage.fromBitmap(makePinBitmap(itemData.Cust,itemData.chtype)!!)
            //marker.setIconTintColor(Color.RED); // 덧입힐 색상
            marker.width = Marker.SIZE_AUTO
            marker.height = Marker.SIZE_AUTO
            //marker.setCaptionMinZoom(12);
            //marker.setCaptionMaxZoom(16);
            marker.captionText = itemData.bldNM
            marker.isHideCollidedCaptions = true // 겹치는 캡션 자동 숨김 처리
            marker.isHideCollidedSymbols = true //  마커와 겹치는 지도 심벌을 자동으로 숨기도록 지정
            //marker.setHideCollidedMarkers(false);
            marker.tag = itemData
            activeMarkers?.add(marker)
            marker.onClickListener = Overlay.OnClickListener { overlay: Overlay ->
                mInfoWindow.open(marker)
                val position = LatLng(itemData.latitude, itemData.longitude) // 현재 위치
                val cameraUpdate = CameraUpdate.scrollTo(position).animate(CameraAnimation.Easing)
                mNaverMap!!.moveCamera(cameraUpdate)
                onClickMarker(overlay)
                true
            }
        }
        handler.post {
            // 메인 스레드
            for (marker in activeMarkers!!) {
                marker.map = mNaverMap
            }
        }
    }
}
 
private fun freeActiveMarkers() {
    if (activeMarkers == nullreturn
    for (marker in activeMarkers!!) {
        marker.map = null // 마커들 지도에서 삭제
    }
}
 

 

 

위 코드를 TedNaverMapClustering 코드로 변경한 코드이다.

import ted.gun0912.clustering.naver.TedNaverClustering

lateinit var tedNaverClustering: TedNaverClustering<NaverItem>
private fun makeClusterMarker(itemList: List<NaverItem>){
    tedNaverClustering = TedNaverClustering.with<NaverItem>(this, mNaverMap)
        .customMarker{ itemData ->
            Marker(itemData.position).apply {
                icon = OverlayImage.fromBitmap(makePinBitmap(itemData.Cust,itemData.chtype)!!)
                captionText = itemData.bldNM!!
                tag = itemData
            }
        }
        .markerClickListener { itemData ->
            val position = itemData.position
            val cameraUpdate = CameraUpdate.scrollTo(position).animate(CameraAnimation.Easing)
            mNaverMap!!.moveCamera(cameraUpdate)
            onClickClusterMarker(itemData.code)
        }
        .minClusterSize(20)
        .make()
 
    val executor = Executors.newSingleThreadExecutor()
    val handler = Handler(Looper.getMainLooper())
    executor.execute {
        // 백그라운드 스레드
        tedNaverClustering.addItems(itemList)
        Log.e("TAG""Number of tedNaverClustering :" + itemList.size)
    } // end executor.execute
}
 

 

fun onClickClusterMarker(code: String?){
    // 호출해야 할 데이터가 다른 ArrayList 에서 code 값이 서로 일치하는 데이터만 추출해야 한다.
    val itemData: ItemData = itemDataList.filter {it.code.equals(code)}.single()
 
}

 

서버에서 가져온 데이터 List 에 추가하는 코드 중에서 핵심만 발췌

response.body()?.let {
    if (it.status.contains("success")) {
        itemDataList.clear() // 서버에서 가져온 데이터 초기화
        naverItemList.clear()
        val itemData: List<ItemData> = it.iteminfo
        Log.e("TAG""Number of itemData :" + itemData.size)
        for (item in itemData) {
            // 자동으로 타입 변환을 하는 듯함.
            itemDataList.add(item)
            putNaverItem(item.latitude,item.longitude,item.code,item.bldNM,item.chtype,item.Cust)
        }
        showMapMarker(itemDataList)
    } else {
        Utils.showAlert(context, it.status, it.message)
    }
 
fun putNaverItem(lat: Double, lng: Double, code: String, bldNM: String, chtype: Int, Cust: Int){
    // NaverItem 생성자에 데이터 추가 및 naverItemList 에 추가
    var naverItem = NaverItem(lat,lngitude,code,bldNM,chtype,Cust)
    naverItemList.add(naverItem)
}

 

코드 구현 이슈

Thread Pool size 를 초과하는 데이터 크기(서버로부터의 Input 데이터 개수)는 좀 더 신경써야 할 거 같다.

 

 

 

블로그 이미지

Link2Me

,
728x90

현재 위치한 반경 km 범위내 데이터를 가져오는 쿼리문

숫자가 직접 기입된 부분을 userLAT, userLNG 로 바꿔줘야 한다.

 

--쿼리문
SELECT *, (6371 * acos(cos(radians(37.504696)) * cos(radians(latitude)) * cos(radians(longitude) - radians(127.041237)) + sin(radians(37.504696))*sin(radians(latitude)))) AS distance 
FROM blding 
HAVING distance <= 0.5 
ORDER BY distance;
 

 

지도를 움직일 경우 ** 정보를 조회해서 화면에 표시될 수 있도록 한다.

지도를 일정 레벨 이상으로 확대했을 경우, 해당 위치에 있는 정보 List를 서버에 요청한다.

onCameraMove → showList() → getDataFromServer() → addMarker()

 

 

 

블로그 이미지

Link2Me

,
728x90

Android Studio 에서 업그레이드하라는 문구 메시지가 나온다.

너무 최신버전으로 업그레이드를 하지 않고 적당한 버전으로 업그레이드를 하려고 했더니 쉽지 않다.

 

처음에 설치된 버전은 Project build.gradle 에서 확인하면 4.1.1 버전이었다.

2021년도에 7.0.2 버전으로의 업그레이드를 한 기억이 있어서 수작업으로 7.0.2 버전으로 설정을 했더니 잘 안된다.

 

 

gradle-wrapper.properties 에서 수작업으로 아래와 같이 수정을 해주었더니 진행이 된다.

 

에러 메시지가 나온다.

 

에러 메시지를 무시하고 해도 되는데 없애는 방법을 적어둔다.

 

이렇게 했음에도 최신버전으로 업그레이드를 하라는 문구는 나오는데 무시했다.

7.1.0 버전으로 코드 작성을 해 본 적이 없어서 무엇을 또 수정해야 할지 몰라서다.

블로그 이미지

Link2Me

,
728x90
선택정렬(Selection Sort)은 해당 순서에 원소를 넣을 위치는 이미 정해져 있고, 그 위치에 어떤 원소를 넣을지 선택하는 알고리즘이다.

오름차순 정렬은

첫 번째 순서에는 첫 번째 위치에 가장 최소값을 넣는다.
두 번째 순서에는 두 번째 위치에 남은 값 중에서의 최소값을 넣는다.

세 번째 순서에는 세 번째 위치에 남은 값 중에서의 최소값을 넣는다.

......

 

1. 주어진 배열 중에서 최소값을 찾는다.
2. 그 값을 맨 앞에 위치한 값과 교환한다.
3. 맨 처음 위치를 뺀 나머지 리스트를 같은 방법으로 교환한다.
하나의 원소만 남을 때까지 위의 1~3 과정을 반복한다.

public class SelectionSort {
    public static void printArray(int[] a){
        for(int i = 0; i < a.length; i++){
            System.out.print("[" + a[i] + "]");
        }
        System.out.print("\n");
    }
 
    public static void main(String[] args) {
        int[] a = {952468165381};
 
        // 정렬 전의 배열 내용을 표시
        System.out.print("정렬 전 배열\n");
        printArray(a);
        System.out.print("\n");
 
        int[] aa = arr(a);
        // 정렬 후의 배열 내용 표시
        System.out.print("정렬 후 배열(오름차순)\n");
        printArray(aa);
 
        System.out.println("************************");
        int[] b = {952468165381};
        int[] bb = brr(b);
        // 정렬 후의 배열 내용 표시
        System.out.print("정렬 후 배열(내림차순)\n");
        printArray(bb);
 
    }
 
    private static int[] arr(int[] a) {
        // 선택 정렬(오름차순)
        int i, var, min, temp;
        for (i = 0; i < a.length -1 ; i++) {
            min = i;
            for (var = i + 1; var < a.length; var++) {
                // 맨 앞의 수를 하나씩 증가시키면서 값 비교하여 조건 비교 및 값 맞바꿈
                System.out.printf("i=%d, var=%d :: ",i, var);
                if (a[var] < a[i]) {
                    // 기준 값(i index)과 변동값(var index)을 비교하여
                    // 기준값(a[i])이 더 크면 맞바꿈 ==> 기준값 자리에 최소값을 넣음
                    min = var;
                    temp = a[i];
                    a[i] = a[min];
                    a[min] = temp;
                    System.out.printf(
                            "min=%d, temp=%d, a[%d]=%d, a[%d]=%d",
                            min,temp, i, a[i], min, a[min]
                    );
                }
                System.out.print("\n");
                printArray(a);
            }
        }
        return a;
    }
 
    private static int[] brr(int[] a) {
        // 선택 정렬(내림차순)
        int i, var, max, temp;
        for (i = 0; i < a.length -1 ; i++) {
            max = i;
            for (var = i + 1; var < a.length; var++) {
                // 맨 앞의 수를 하나씩 증가시키면서 값 비교하여 조건 비교 및 값 맞바꿈
                System.out.printf("i=%d, var=%d :: ",i, var);
                if (a[var] > a[i]) { // 배열 앞의 값이 더 크면
                    max = var;
                    temp = a[i];
                    a[i] = a[max];
                    a[max] = temp;
                    System.out.printf(
                            "max=%d, temp=%d, a[%d]=%d, a[%d]=%d",
                            max,temp, i, a[i], max, a[max]
                    );
                }
                System.out.print("\n");
                printArray(a);
            }
        }
        return a;
    }
 
}
 

 

 

'안드로이드 > Java 알고리즘' 카테고리의 다른 글

Java 삽입정렬(Insertion Sort)  (0) 2022.01.10
Java 버블정렬(Bubble Sort)  (0) 2022.01.10
블로그 이미지

Link2Me

,
728x90

삽입 정렬이란 2번째 원소부터 n번째 원소까지 차례로 해당 원소가 위치할 인덱스에 원소를 삽입하는 방식을 사용하는 정렬 방식이다.

 

public class InsertionSort {
    public static void printArray(int[] a){
        for(int i : a){
            System.out.print("[" + i + "]");
        }
        System.out.print("\n");
    }
 
    public static void main(String[] args) {
        int[] a = {952468165381};
 
        // 정렬 전의 배열 내용을 표시
        System.out.print("정렬 전 배열\n");
        printArray(a);
        System.out.print("\n");
 
        int[] aa = arr(a);
        // 정렬 후의 배열 내용 표시
        System.out.print("정렬 후 배열(오름차순)\n");
        printArray(aa);
 
        System.out.print("\n");
 
        int[] bb = crr(a);
        // 정렬 후의 배열 내용 표시
        System.out.print("정렬 후 배열(내림차순)\n");
        printArray(bb);
    }
 
    public static int[] arr (int[] a){
        int i, prev, temp;
        // 삽입 정렬(오름차순)
        // 삽입 정렬이란 2번째 원소부터 n번째 원소까지 차례로
        // 해당 원소가 위치할 인덱스에 원소를 삽입하는 방식을 사용하는 정렬 방식
        for(i = 1; i < a.length; i++){
            temp = a[i];
            System.out.printf("a[%d]=%d(key) :: ",i, a[i]);
            for(prev = i -1; prev >= 0; prev--){
                if(a[prev] > temp){ // 이전 원소의 배열값이 key(temp)값보다 더 크면
                    a[prev + 1= a[prev]; // 자리 바꿈
                    System.out.printf(
                            "i=%d, prev=%d, temp=%d(key), a[%d]=%d",
                            i,prev,temp,prev+1,a[prev+1]
                    );
                } else {
                    break;
                }
                System.out.print("\n");
            }
            a[prev + 1= temp;
            System.out.printf("i=%d, prev=%d, a[%d]=%d\n",i, prev, prev+1, temp);
            printArray(a);
        }
        return a;
    }
 
    public static int[] brr (int[] a){
        int i, prev, temp;
        // 삽입 정렬(내림차순)
        for(i = 1; i < a.length; i++){
            temp = a[i];
            for(prev = i -1; prev >= 0 && a[prev] < temp; prev--){
                a[prev + 1= a[prev];
            }
            a[prev + 1= temp;
        }
        return a;
    }
 
    public static int[] crr (int[] a){
        int i, prev, temp;
        // 삽입 정렬(내림차순)
        for(i = 1; i < a.length; i++){
            temp = a[i]; // 현재 선택된 원소의 값을 임시 변수에 저장해준다.
            prev = i - 1// 현재 원소를 기준으로 이전 원소를 탐색하기 위한 index 변수
            // 현재 선택된 원소가 이전 원소보다 큰 경우까지만 반복. 단, 0번째 원소까지만 비교
            while (prev >= 0 && a[prev] < temp){
                a[prev + 1= a[prev];
                prev--;
            }
            // 탐색이 종료된 지점에 현재 선택되었던 변수의 값을 삽입해준다
            a[prev + 1= temp;
        }
        return a;
    }
}
 
 

 

 

'안드로이드 > Java 알고리즘' 카테고리의 다른 글

Java 선택 정렬(Selection Sort)  (0) 2022.01.11
Java 버블정렬(Bubble Sort)  (0) 2022.01.10
블로그 이미지

Link2Me

,
728x90

버블 정렬이란, 이웃한 두 요소의 대소 관계를 비교하여 교환을 반복하는 정렬 알고리즘을 뜻한다. 인접한 2가지 요소의 대소 관계를 검사하여 원하는 순서대로 되어 있지 않은 경우 교환을 하는 것이다. 오름차순, 내림차순으로 정렬할 수 있다.

 

public class BubbleSort {
    public static void printArray(int[] a){
//        for(int i = 0; i < a.length; i++){
//            System.out.print("[" + a[i] + "]");
//        }
        for(int i: a){
            System.out.print("["+i+"]");
        }
        System.out.print("\n");
    }
 
    public static void main(String[] args) {
        int[] a = {952468165381};
 
        // 정렬 전의 배열 내용을 표시
        System.out.print("정렬 전 배열\n");
        printArray(a);
        System.out.print("\n");
 
        int[] aa = arr(a);
        // 정렬 후의 배열 내용 표시
        System.out.print("정렬 후 배열(오름차순)\n");
        printArray(aa);
 
        int[] bb = brr(a);
        // 정렬 후의 배열 내용 표시
        System.out.print("정렬 후 배열(내림차순)\n");
        printArray(bb);
 
    }
 
    private static int[] arr(int[] a) {
        // 버블 정렬(오름차순)
        int i, j, temp;
        for (i = 0; i < a.length -1 ; i++) {
            for (j = 0; j < a.length - i -1; j++) {
                // 맨 뒤가 큰 수가 되도록 이동하고, 맨 뒷수를 하나씩 줄이기
                System.out.printf("i=%d, j=%d :: ",i, j);
                if (a[j] > a[j+1]) { // 배열 앞의 값이 더 크면
                    temp = a[j];
                    a[j] = a[j+1];
                    a[j+1= temp;
                    System.out.printf(
                            "j=%d, temp=%d, a[%d]=%d, a[%d]=%d",
                            j,temp, j, a[j], j+1, a[j+1]
                    );
                }
                System.out.print("\n");
                printArray(a);
            }
        }
        return a;
    }
 
    private static int[] brr(int[] a) {
        // 버블 정렬(내림차순)
        int i, j, temp;
        for (i = 0; i < a.length -1 ; i++) {
            for (j = 0; j < a.length - i -1; j++) {
//                System.out.printf("i=%d, j=%d :: ",i, j);
                if (a[j] < a[j+1]) { // 배열 앞의 값이 더 작으면
                    temp = a[j];
                    a[j] = a[j+1];
                    a[j+1= temp;
//                    System.out.printf(
//                            "j=%d, temp=%d, a[%d]=%d, a[%d]=%d",
//                            j,temp, j, a[j], j+1, a[j+1]
//                    );
                }
//                System.out.print("\n");
//                printArray(a);
            }
        }
        return a;
    }
 
}
 

 

'안드로이드 > Java 알고리즘' 카테고리의 다른 글

Java 선택 정렬(Selection Sort)  (0) 2022.01.11
Java 삽입정렬(Insertion Sort)  (0) 2022.01.10
블로그 이미지

Link2Me

,