Kotlin 마이그레이션

gang_shik·2022년 7월 4일
1

프로젝트 Fit-In

목록 보기
7/10

Kotlin으로 변경

  • 이전에 Java로 개발하는데 있어서 가장 큰 문제는 스파게티 코드와 보일러 플레이트 코드였음

  • 서로가 서로를 참조하고 생성자부터해서 필요 이상으로 코드가 늘어지는 현상이 발생했음

  • 물론 이 부분을 1차적으로 MVVM 패턴을 적용시켜서 개선을 하였지만 이전에 경험에 빗대어서 이를 Kotlin 자체로 개선하는 것이 좀 더 효율적이라고 생각이 되어 Kotlin 적용 및 DI 패턴 적용과 코루틴을 접목시켜서 전반적으로 Kotlin으로 마이그레이션을 진행함

개선사항

  • 여러가지 코드적으로 개선한 부분이 있지만 코틀린 버전을 사용함에 따라 공식문서에서 제시하는 개발적인 부분을 힘입어 도움을 받은 부분이 몇 개 있음

Data Class

  • 기존의 데이터 클래스의 경우 Parcelable을 implements함과 동시에 관련 메소드까지 오버라이딩 하여서 처리함
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);
    }
}
  • 이 부분을 Kotlin에선 어노테이션을 통해서 상당히 코드 개선을 할 수 있었음, 어노테이션을 통해 Java에서 직접 오버라이딩을 하여 쓴 부분을 컴파일러가 자동으로 생성해서 만들어줌
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
  • Adapter의 경우도 생성자 관리와 DiffUtil 클래스를 별도로 만들어서 관리하고 클릭 리스너 처리 같은 경우도 인터페이스로 처리를 했음
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)
    }

}
  • Fragment와 ViewModel도 로직은 큰 다름이 없지만 좀 더 개선사항이 생겼다면 리소스를 불러올 때 getter/setter를 생략하고 바로 변수에 접근해서 처리할 수 있는 부분이 코드적으로 불필요하게 길어지고 헷갈릴만한 요소를 개선해줌
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();
    }
}
  • 위와 같이 getter를 통해서 접근하기 위해서 추가적인 메소드와 처리 작업이 존재했으나 아래와 같이 Kotlin에선 단순하게 변수로 접근해서 처리하여서 좀 더 간결해졌음
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를 적용할 수 있음

profile
측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 개선시킬 수도 없다

1개의 댓글

comment-user-thumbnail
2024년 2월 21일

공유 감사합니다

답글 달기