이전에 Java로 개발하는데 있어서 가장 큰 문제는 스파게티 코드와 보일러 플레이트 코드였음
서로가 서로를 참조하고 생성자부터해서 필요 이상으로 코드가 늘어지는 현상이 발생했음
물론 이 부분을 1차적으로 MVVM 패턴을 적용시켜서 개선을 하였지만 이전에 경험에 빗대어서 이를 Kotlin 자체로 개선하는 것이 좀 더 효율적이라고 생각이 되어 Kotlin 적용 및 DI 패턴 적용과 코루틴을 접목시켜서 전반적으로 Kotlin으로 마이그레이션을 진행함
package com.example.fitin_v2.model;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.gson.annotations.SerializedName;
public class AccountRequestDto implements Parcelable {
@SerializedName("email")
private String email;
@SerializedName("password")
private String password;
@SerializedName("name")
private String name;
public String getEmail() {
return email;
}
public String getPassword() {
return password;
}
public String getName() {
return name;
}
public AccountRequestDto(String email, String password, String name) {
this.email = email;
this.password = password;
this.name = name;
}
protected AccountRequestDto(Parcel in) {
email = in.readString();
password = in.readString();
name = in.readString();
}
public static final Creator<AccountRequestDto> CREATOR = new Creator<AccountRequestDto>() {
@Override
public AccountRequestDto createFromParcel(Parcel in) {
return new AccountRequestDto(in);
}
@Override
public AccountRequestDto[] newArray(int size) {
return new AccountRequestDto[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(email);
parcel.writeString(password);
parcel.writeString(name);
}
}
package com.example.fitin_kotlin.data.model.network.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize
@Parcelize
data class RequestSignUp(
@SerializedName("email")
val email: String?,
@SerializedName("password")
val password: String?,
@SerializedName("name")
val name: String?
) : Parcelable
package com.example.fitin_v2.ui.home;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.example.fitin_v2.databinding.ListItemNewsBinding;
import com.example.fitin_v2.model.NewsListResponseDto;
import org.jetbrains.annotations.NotNull;
public class HomeAdapter extends ListAdapter<NewsListResponseDto, HomeAdapter.NewsListViewHolder> {
private OnItemClickListener listener;
protected HomeAdapter(@NonNull DiffUtil.ItemCallback<NewsListResponseDto> diffCallback) {
super(diffCallback);
}
@NonNull
@Override
public NewsListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
ListItemNewsBinding binding = ListItemNewsBinding.inflate(inflater, parent, false);
return new NewsListViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull NewsListViewHolder holder, int position) {
final NewsListResponseDto newsListResponseDto = this.getItem(position);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
listener.onItemClick(newsListResponseDto);
}
});
holder.bind(newsListResponseDto);
}
static class NewsListViewHolder extends RecyclerView.ViewHolder {
private final ListItemNewsBinding binding;
public NewsListViewHolder(@NonNull ListItemNewsBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public final void bind(@NotNull NewsListResponseDto newsListResponseDto) {
binding.setProperty(newsListResponseDto);
binding.executePendingBindings();
}
}
public interface OnItemClickListener {
void onItemClick(NewsListResponseDto newsListResponseDto);
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener;
}
}
package com.example.fitin_kotlin.ui.home
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.fitin_kotlin.data.model.network.response.ResponseNewsList
import com.example.fitin_kotlin.databinding.ListItemNewsBinding
class HomeAdapter(private val onClickListener: OnClickListener) : ListAdapter<ResponseNewsList, HomeAdapter.NewsListViewHolder>(DiffCallback){
class NewsListViewHolder(private var binding: ListItemNewsBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(responseNewsList: ResponseNewsList) {
binding.newsProperty = responseNewsList
binding.executePendingBindings()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsListViewHolder {
val inflater: LayoutInflater = LayoutInflater.from(parent.context)
val listItemNewsBinding: ListItemNewsBinding = ListItemNewsBinding.inflate(inflater, parent, false)
return NewsListViewHolder(listItemNewsBinding)
}
override fun onBindViewHolder(holder: NewsListViewHolder, position: Int) {
val responseNewsList = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(responseNewsList)
}
holder.bind(responseNewsList)
}
companion object DiffCallback : DiffUtil.ItemCallback<ResponseNewsList>() {
override fun areItemsTheSame(
oldItem: ResponseNewsList,
newItem: ResponseNewsList
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: ResponseNewsList,
newItem: ResponseNewsList
): Boolean {
return oldItem.id == newItem.id
}
}
class OnClickListener(val clickListener: (responseNewsList:ResponseNewsList) -> Unit) {
fun onClick(responseNewsList: ResponseNewsList) = clickListener(responseNewsList)
}
}
package com.example.fitin_v2.ui.onboard.signin;
import android.app.Application;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.fitin_v2.model.AccountLoginDto;
import com.example.fitin_v2.repository.NewsRepository;
import com.example.fitin_v2.repository.UserRepository;
import io.reactivex.disposables.CompositeDisposable;
public class SignInViewModel extends AndroidViewModel {
private UserRepository userRepository;
private NewsRepository newsRepository;
private final CompositeDisposable disposable = new CompositeDisposable();
public MutableLiveData<String> email = new MutableLiveData<>();
public MutableLiveData<String> password = new MutableLiveData<>();
private MutableLiveData<AccountLoginDto> userMutableLiveData;
private final MutableLiveData<Boolean> _eventSignIn = new MutableLiveData<Boolean>();
LiveData<Boolean> eventSignIn;
private final MutableLiveData<Boolean> _eventBack = new MutableLiveData<Boolean>();
LiveData<Boolean> eventBack;
public SignInViewModel(@NonNull Application application) {
super(application);
userRepository = new UserRepository(application);
newsRepository = new NewsRepository(application);
}
public MutableLiveData<AccountLoginDto> getUser() {
if (userMutableLiveData == null) {
userMutableLiveData = new MutableLiveData<>();
}
return userMutableLiveData;
}
public LiveData<Boolean> getBack() {
return eventBack = _eventBack;
}
public void onBack() {
_eventBack.setValue(true);
}
public void onBackComplete() {
_eventBack.setValue(false);
}
public LiveData<Boolean> getEventSignIn() {
return eventSignIn = _eventSignIn;
}
public void onEventSignInComplete() {
_eventSignIn.setValue(false);
}
public void onLogin(View view) {
AccountLoginDto accountLoginDto = new AccountLoginDto(email.getValue(), password.getValue());
userRepository.getToken(accountLoginDto);
newsRepository.callNews();
_eventSignIn.setValue(true);
}
@Override
protected void onCleared() {
super.onCleared();
disposable.clear();
}
}
package com.example.fitin_v2.ui.onboard.signin;
import android.content.Intent;
import android.os.Bundle;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.example.fitin_v2.R;
import com.example.fitin_v2.databinding.FragmentSignInBinding;
import com.example.fitin_v2.ui.home.HomeActivity;
public class SignInFragment extends Fragment {
private FragmentSignInBinding binding;
private SignInViewModel viewModel;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_sign_in, container, false);
viewModel = new ViewModelProvider(this).get(SignInViewModel.class);
binding.setLifecycleOwner(this);
binding.setSigninViewModel(viewModel);
viewModel.getBack().observe(getViewLifecycleOwner(), back -> {
if(back) {
NavHostFragment.findNavController(SignInFragment.this).navigate(SignInFragmentDirections.actionSignInFragmentToMainFragment());
viewModel.onBackComplete();
}
});
viewModel.getEventSignIn().observe(getViewLifecycleOwner(), signIn -> {
if (signIn) {
Intent intentHome = new Intent(getActivity(), HomeActivity.class);
startActivity(intentHome);
getActivity().overridePendingTransition(0,0);
getActivity().finish();
viewModel.onEventSignInComplete();
}
});
return binding.getRoot();
}
}
package com.example.fitin_kotlin.ui.onboard.signin
import android.util.Log
import android.view.View
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.fitin_kotlin.data.local.EncryptedSharedPreferenceController
import com.example.fitin_kotlin.data.model.network.request.RequestSignIn
import com.example.fitin_kotlin.data.repository.NewsRepository
import com.example.fitin_kotlin.data.repository.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SignInViewModel @Inject constructor(
private val userRepository: UserRepository,
private val newsRepository: NewsRepository,
private val prefs: EncryptedSharedPreferenceController
) : ViewModel(){
val email: MutableLiveData<String> = MutableLiveData<String>()
val password: MutableLiveData<String> = MutableLiveData<String>()
private val _eventSignIn = MutableLiveData<Boolean>()
val eventSignIn: LiveData<Boolean>
get() = _eventSignIn
fun onSignIn(view: View) {
val request = RequestSignIn(email.value, password.value)
viewModelScope.launch {
val signin = userRepository.postSignIn(request)
when (signin.isSuccessful) {
true -> {
Log.e("token", "성공: " + signin.body()?.accessToken)
prefs.setAccessToken(signin.body()!!.accessToken)
prefs.setRefreshToken(signin.body()!!.refreshToken)
newsRepository.callNews()
}
else -> {
Log.e("실패", "error " + signin.message())
}
}
}
_eventSignIn.value = true
}
fun onEventSignInComplete() {
_eventSignIn.value = false
}
private val _eventBack = MutableLiveData<Boolean>()
val eventBack: LiveData<Boolean>
get() = _eventBack
fun onBack() {
_eventBack.value = true
}
fun onBackComplete() {
_eventBack.value = false
}
}
package com.example.fitin_kotlin.ui.onboard.signin
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import com.example.fitin_kotlin.R
import com.example.fitin_kotlin.databinding.FragmentSignInBinding
import com.example.fitin_kotlin.ui.home.HomeActivity
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SignInFragment : Fragment() {
private val signInViewModel: SignInViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val binding: FragmentSignInBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_sign_in, container, false)
binding.lifecycleOwner = viewLifecycleOwner
binding.signInViewModel = signInViewModel
signInViewModel.eventBack.observe(viewLifecycleOwner, Observer { back ->
if (back) {
findNavController().navigate(SignInFragmentDirections.actionSignInFragmentToWelcomeFragment())
signInViewModel.onBackComplete()
}
})
signInViewModel.eventSignIn.observe(viewLifecycleOwner, Observer { signIn ->
if (signIn) {
val intent = Intent(activity, HomeActivity::class.java)
startActivity(intent)
activity?.overridePendingTransition(0,0)
activity?.finish()
signInViewModel.onEventSignInComplete()
}
})
return binding.root
}
}
Kotlin으로 개선함과 동시에 추가적인 라이브러리 도입과 몇가지 로직이 변경된 사항이 있음
우선 가장 큰 것은 DI 패턴과 네트워크 통신으로 볼 수 있음
DI 패턴의 경우 위의 Java 코드를 보았다시피 new
를 통해 기존에 있는 객체를 이곳저곳에서 계속 생성하는 보일러 플레이트 코드와 무엇보다 Context 부분과 관련해서 현재 Preferences를 어디서든 쓸 수 있게 하기 위해서 Application
단위의 클래스를 통해서 호출하게 하였는데 중요한 것은 이 부분에 대해서 계속적으로 생성자를 통해서 만드는 것임
당장은 문제가 되지 않을 수 있지만 이 부분에 있어서 메모리 누수나 성능적인 이슈가 생길 수 있다고 판단하여 MVVM + DI로 많이 접목시킨 케이스를 봤기 때문에 이 부분을 적용시켰음
추가로 Java 버전에서는 RxJava를 통해 Reactive Programming을 통해 작업을 처리했지만 Kotlin에는 Coroutine이라는 비동기 처리에 유용한 부분이 있어서 만약 Java로만 했을 경우 Thread 처리나 Callback 방식이 있는데 이 부분보다 좀 더 유용하게 처리할 수 있기 때문에 접목함
물론 Reactive Programming이 나쁘거나 안 좋아서 바꾼 것이 아니기 때문에 추후 상황을 봐서 Flow를 적용할 수 있음
공유 감사합니다