Skip to content

4.4.0 구조 설계

JI YOON LEE edited this page Mar 1, 2023 · 1 revision

WrittenBy Write About


구현 방법


📦 com.ssafy.smile
 ┣ 📂 data
 ┃ ┗ 📂 local
 ┃ ┃ ┗ 📂 datasource
 ┃ ┃ ┗ 📂 repository
 ┃ ┗ 📂 remote
 ┃ ┃ ┗ 📂 model
 ┃ ┃ ┗ 📂 datasource
 ┃ ┃ ┗ 📂 repository
 ┃ ┃ ┗ 📂 datasource
 ┃ ┃ ┗ 📂 service
 ┣ 📂 domain
 ┃ ┗ 📂 model
 ┃ ┗ 📂 repository
 ┣ 📂 presentation





3. BaseStructure with MVVM


📦 com.ssafy.smile
 ┣ 📂 base
 ┃ ┗ 📜 BaseRepository.kt
 ┃ ┗ 📜 BaseViewModel.kt
 ┃ ┗ 📜 BaseView.kt
 ┃ ┗ 📜 BaseViewImpl.kt
 ┃ ┗ 📜 BaseFragment.kt
 ┃ ┗ 📜 BaseBottomSheetDialogFragment.kt
구현 코드
  • BaseRepository.kt
open class BaseRepository {
    suspend fun <T : Any> safeApiCall(liveData: SingleLiveData<NetworkResponse<T>>, action: suspend () -> Response<T>){
        liveData.postValue(NetworkResponse.Loading())
        val result : NetworkResponse<T> = safeApiResult(action=action)
        liveData.postValue(result)
    }

    private suspend fun <T : Any> safeApiResult(action: suspend () -> Response<T>): NetworkResponse<T> {
        val response = action.invoke()
        Log.d(TAG, "safeApiResult: $response")
        if (response.isSuccessful && response.body()!=null) return NetworkResponse.Success(response.body()!!)
        return NetworkResponse.Failure(response.code())
    }

}

  • BaseViewModel.kt
abstract class BaseViewModel : ViewModel() {

    private val _onBackPressed = SingleLiveData<Any>(null)
    val onBackPressed: SingleLiveData<Any> get() = _onBackPressed
    private var mBackPressedAt = 0L

    private val _error = SingleLiveData<Event<String>>(null)
    val error: SingleLiveData<Event<String>> = _error


    fun onBackPressed() {
        if (mBackPressedAt + TimeUnit.SECONDS.toMillis(2) > System.currentTimeMillis()) {
            _onBackPressed.postValue(true)
        } else {
            _onBackPressed.postValue("\'뒤로\' 버튼을 한번 더 누르시면 종료됩니다.")
            mBackPressedAt = System.currentTimeMillis()
        }
    }

    fun handleError(exception: Throwable) {
        val message = exception.message ?: ""
        _error.value = Event(message)
    }

    fun makeMultiPartBody(jsonKey: String, file: File) : MultipartBody.Part {
        val requestBody = file.convertToRequestBody()
        return MultipartBody.Part.createFormData(jsonKey, file.name, requestBody)
    }
    fun makeMultiPartBodyList(jsonKey: String, images: List<File>): List<MultipartBody.Part> {
        val imageList = arrayListOf<MultipartBody.Part>()
        for (i in images.indices) {
            val multipartBody = makeMultiPartBody(jsonKey, images[i])
            imageList.add(multipartBody)
        }
        return imageList
    }

}

  • BaseView.kt
interface BaseView {
    fun Toolbar.initToolbar(toolbarTitle:String, isBackAvailable:Boolean?=false, backAction : (() -> Unit)?=null )
    fun showLoadingDialog(context: Context){}
    fun dismissLoadingDialog(){}
    fun showDialog(dialog: Dialog, lifecycleOwner: LifecycleOwner?, cancelable: Boolean = true, dismissHandler: (() -> Unit)? = null){}
    fun showToast(context: Context, message: String, type : Types.ToastType?=null, iconEnable : Boolean=true)
}

  • BaseViewImpl.kt
interface BaseViewImpl : BaseView {

    var mLoadingDialog: LoadingDialog

    override fun Toolbar.initToolbar(toolbarTitle:String, isBackAvailable:Boolean?, backAction : (() -> Unit)?){
        this.title = toolbarTitle
        if (isBackAvailable==true) this.setNavigationIcon(R.drawable.ic_back)
        this.setNavigationOnClickListener {
            if (backAction != null) { backAction() }
        }
    }

    override fun showLoadingDialog(context: Context) {
        mLoadingDialog = LoadingDialog(context)
        mLoadingDialog.show()
    }
    override fun dismissLoadingDialog() {
        if (mLoadingDialog.isShowing) {
            mLoadingDialog.dismiss()
        }
    }
    override fun showDialog(dialog: Dialog, lifecycleOwner: LifecycleOwner?, cancelable: Boolean, dismissHandler: (() -> Unit)?) {
        val targetEvent = if (cancelable) Lifecycle.Event.ON_STOP else Lifecycle.Event.ON_DESTROY
        val observer = LifecycleEventObserver { _: LifecycleOwner, event: Lifecycle.Event ->
            if (event == targetEvent && dialog.isShowing) {
                dialog.dismiss()
                dismissHandler?.invoke()
            }
        }
        dialog.show()
        lifecycleOwner?.lifecycle?.addObserver(observer)
        dialog.setOnDismissListener { lifecycleOwner?.lifecycle?.removeObserver(observer) }
    }


    override fun showToast(context: Context, message: String, type : Types.ToastType?, iconEnable : Boolean) {
        when (type){
            Types.ToastType.CUSTOM -> Toasty.warning(context, message, Toast.LENGTH_SHORT).show()
            Types.ToastType.INFO -> Toasty.info(context, message, Toast.LENGTH_SHORT, iconEnable).show()
            Types.ToastType.ERROR -> Toasty.error(context, message, Toast.LENGTH_SHORT, iconEnable).show()
            Types.ToastType.SUCCESS -> Toasty.success(context, message, Toast.LENGTH_SHORT, iconEnable).show()
            Types.ToastType.WARNING -> Toasty.warning(context, message, Toast.LENGTH_SHORT, iconEnable).show()
            else -> Toasty.normal(context, message, Toast.LENGTH_SHORT).show()
        }
    }

}

  • BaseFragment.kt
abstract class BaseFragment<B : ViewBinding>(private val bind: (View) -> B, @LayoutRes layoutResId: Int) : Fragment(layoutResId), BaseViewImpl {

    override lateinit var mLoadingDialog: LoadingDialog
    lateinit var mainActivity : MainActivity

    private var _binding: B? = null
    val binding get() = _binding?: throw IllegalStateException("binding fail")

    override fun onAttach(context: Context) {
        super.onAttach(context)
        mLoadingDialog = LoadingDialog(requireContext())
        mainActivity = context as MainActivity
        mLoadingDialog = LoadingDialog(context)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        _binding = bind(super.onCreateView(inflater, container, savedInstanceState)!!)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initView()
        setEvent()
    }

    abstract fun initView()
    abstract fun setEvent()

    override fun onDestroyView() {
        _binding = null
        super.onDestroyView()
    }
}

  • BaseBottomSheetDialogFragment.kt
abstract class BaseBottomSheetDialogFragment<B : ViewBinding>(private val bindingInflater: (layoutInflater:LayoutInflater) -> B)
    : BottomSheetDialogFragment(), BaseViewImpl {

    override lateinit var mLoadingDialog: LoadingDialog

    private var _binding: B? = null
    val binding get() = _binding?: throw IllegalStateException("binding fail")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mLoadingDialog = LoadingDialog(requireContext())
        setStyle(DialogFragment.STYLE_NORMAL, R.style.CustomBottomSheetDialogTheme)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        _binding = bindingInflater.invoke(inflater)
        return _binding?.root
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
        dialog.setOnShowListener {
            val bottomSheet = dialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
            setupRatio(bottomSheet!!)
        }
        return dialog
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initView()
        setEvent()
    }

    abstract fun initView()
    abstract fun setEvent()

    override fun onDestroyView() {
        _binding = null
        super.onDestroyView()
    }

    fun setupRatio(view : View, ratio:Int=30){
        val layoutParams = view.layoutParams
        layoutParams.height = getBottomSheetDialogDefaultHeight(ratio)
        view.layoutParams = layoutParams
    }

    private fun getBottomSheetDialogDefaultHeight(ratio:Int): Int { return getWindowHeight() * ratio / 100 }

    private fun getWindowHeight(): Int {
        val displayMetrics = DisplayMetrics()
        requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
        return displayMetrics.heightPixels
    }

}

Clone this wiki locally