관리 메뉴

막내의 막무가내 프로그래밍 & 일상

[안드로이드] 안드로이드 MVVM + RxJava2 + Koin + Jsoup 정리 (충남대학교 컴퓨터공학과 공지사항 토이 프로젝트) 본문

안드로이드/코틀린 & 아키텍처 & Recent

[안드로이드] 안드로이드 MVVM + RxJava2 + Koin + Jsoup 정리 (충남대학교 컴퓨터공학과 공지사항 토이 프로젝트)

막무가내막내 2020. 6. 3. 17:04
728x90

 

 

 

[2021-04-07 업데이트]

 

매번 공지사항 들어가서 보기 귀찮아서 공지사항 앱을 1차로 만들어봤습니다. 

추후 시간이 될때 클릭시 웹이 아닌 앱내에서 웹뷰로 띄워주거나 커뮤니티 기능도 추가해볼까 합니다. -> 업데이트 완료!

 

MVVM 아키텍처를 적용 및 학습을 위해 대학교 공지사항 토이프로젝트를 만들어봤었는데 그것에 대해 복습 및 정리 해볼려고 합니다.

API 를 사용한 것이 아니라 Jsoup 을 통해 크롤링하여 데이터를 가져온 것이기 때문에 레트로핏은 사용하지 않았습니다.

저도 배워가는 입장이라 수정이 필요한 부분을 지적해주시면 감사하겠습니다.

 

그래서 전체적인 프로젝트 구조는 다음과 같습니다.

MVVM 의 장점과 단점은 다음과 같습니다.

장점: 뷰와 모델간의 의존성이 없고 MVP 패턴처럼    V-VM 1:1 관계가 아닌 독립적이기 때문에 사이의 의존성도 해결된다. 데이터 바인딩을 통해 V-VM 의존성도 해결할 있습니다.

딘점 : VIewModel 설계가 어렵다.

 

 

전체 프로젝트는 다음 링크에서 볼 수 있습니다.

https://github.com/mtjin/cnu-notice-app-releaseversion

 

mtjin/cnu-notice-app-releaseversion

충남대 공지앱 출시버전. Contribute to mtjin/cnu-notice-app-releaseversion development by creating an account on GitHub.

github.com

 

Retrofit2 를 사용한 프로젝트는 다음과 같습니다. 

https://github.com/mtjin/android-architecture-study-movieapp/tree/master/BACK_UP/9-RxJava/AndroidArchitectureStudy

 

mtjin/android-architecture-study-movieapp

안드로이드 아키텍처 스터디 정리. Contribute to mtjin/android-architecture-study-movieapp development by creating an account on GitHub.

github.com

 

(이 프로젝트는 RxJava2는 사용하지 않았습니다.)

https://github.com/mtjin/CoronaKorea

 

mtjin/CoronaKorea

한국 코로나 정보 애플리케이션. Contribute to mtjin/CoronaKorea development by creating an account on GitHub.

github.com

 

 

1. 먼저 BaseActivity, BaseFragment, BaseViewModel 을 만들어 주었습니다. 데이터바인딩, 공통함수 등을 관리하기 쉽게 해줍니다. 

 

[BaseActivity]

package com.mtjin.cnunoticeapp.base

import android.os.Bundle
import android.widget.Toast
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import io.reactivex.disposables.CompositeDisposable

abstract class BaseActivity<B : ViewDataBinding>(
    @LayoutRes val layoutId: Int
) : AppCompatActivity() {
    lateinit var binding: B
    private val compositeDisposable = CompositeDisposable()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, layoutId)
        binding.lifecycleOwner = this
    }

    protected fun showToast(msg: String) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
    }

    override fun onDestroy() {
        super.onDestroy()
        compositeDisposable.clear()
    }
}

 

[BaseFragment]

viewModel 을 제네릭으로 넘기는데 이 부분은 안넘기고 그냥 프래그먼트에서 해결해도 됩니다. 

Koin 사용시 val viewModel: BusinessNoticeViewModel by viewModel()  이런식으로 Base말고 실제 프래그먼트에서 해주면 됩니다. 

실제 프래그먼트에서는 init()에 뷰를 초기화하는 작업을 override 해주도록 합니다.

package com.mtjin.cnunoticeapp.base

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.annotation.LayoutRes
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment

abstract class BaseFragment<B : ViewDataBinding, VM : BaseViewModel>(
    @LayoutRes val layoutId: Int
) : Fragment() {
    protected lateinit var binding: B
    protected abstract val viewModel: VM

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(inflater, layoutId, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.lifecycleOwner = this
        init()
    }

    abstract fun init()

    protected fun showToast(msg: String) =
        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
}

 

 

[BaseViewModel]

RxJava2 관리를 하기 쉽게 CompositeDiasposable() 을 사용하고 onCleraed() 될 때 해제해주도록 합니다.

package com.mtjin.cnunoticeapp.base

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import io.reactivex.disposables.CompositeDisposable

abstract class BaseViewModel : ViewModel() {
    protected val compositeDisposable = CompositeDisposable()

    private val _isLoading = MutableLiveData<Boolean>(false)
    val isLoading: LiveData<Boolean> get() = _isLoading

    override fun onCleared() {
        compositeDisposable.dispose()
        super.onCleared()
    }

    protected fun showProgress() {
        _isLoading.value = true
    }

    protected fun hideProgress() {
        _isLoading.value = false
    }
}

 

 


 

2. Koin 사용을 위한 Application 과 Module 을 정의해줍니다.

의존성 주입을 사용함으로써 결합도를 낮춰주고 유연성 및 확장성은 높여주어 개발도 편리하고 유지보수하기 쉽게 만들어주므로 적극 사용하도록 합니다.

manifest

[KoinApplicaion]

package com.mtjin.cnunoticeapp.di

import android.app.Application
import com.mtjin.cnunoticeapp.BuildConfig
import com.mtjin.cnunoticeapp.module.*
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import org.koin.core.logger.Level

class KoinApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            if (BuildConfig.DEBUG) {
                androidLogger()
            } else {
                androidLogger(Level.NONE)
            }
            androidContext(this@KoinApplication)
            modules(
                repositoryModule,
                localDataModule,
                remoteDataModule,
                viewModelModule,
                apiModule
            )

        }
    }
}

 

[Modules]

각각의 모듈들을 정의해줍니다.

package com.mtjin.cnunoticeapp.module

import org.koin.core.module.Module
import org.koin.dsl.module

val apiModule: Module = module {
    //파싱할일만 있어서 일단은 안쓸듯
}
package com.mtjin.cnunoticeapp.module

import androidx.room.Room
import com.mtjin.cnunoticeapp.data.bachelor.source.local.BachelorNoticeDao
import com.mtjin.cnunoticeapp.data.bachelor.source.local.BachelorNoticeLocalDataSource
import com.mtjin.cnunoticeapp.data.bachelor.source.local.BachelorNoticeLocalDataSourceImpl
import com.mtjin.cnunoticeapp.data.business.source.local.BusinessNoticeDao
import com.mtjin.cnunoticeapp.data.business.source.local.BusinessNoticeLocalDataSource
import com.mtjin.cnunoticeapp.data.business.source.local.BusinessNoticeLocalDataSourceImpl
import com.mtjin.cnunoticeapp.data.employ.source.local.EmployNoticeDao
import com.mtjin.cnunoticeapp.data.employ.source.local.EmployNoticeLocalDataSource
import com.mtjin.cnunoticeapp.data.employ.source.local.EmployNoticeLocalDataSourceImpl
import com.mtjin.cnunoticeapp.data.favorite.source.local.FavoriteNoticeDao
import com.mtjin.cnunoticeapp.data.favorite.source.local.FavoriteNoticeLocalDataSource
import com.mtjin.cnunoticeapp.data.favorite.source.local.FavoriteNoticeLocalDataSourceImpl
import com.mtjin.cnunoticeapp.data.general.source.local.GeneralNoticeDao
import com.mtjin.cnunoticeapp.data.general.source.local.GeneralNoticeLocalDataSource
import com.mtjin.cnunoticeapp.data.general.source.local.GeneralNoticeLocalDataSourceImpl
import com.mtjin.cnunoticeapp.database.NoticeDatabase
import org.koin.core.module.Module
import org.koin.dsl.module

val localDataModule: Module = module {
    single<BachelorNoticeLocalDataSource> { BachelorNoticeLocalDataSourceImpl(get()) }
    single<GeneralNoticeLocalDataSource> { GeneralNoticeLocalDataSourceImpl(get()) }
    single<BusinessNoticeLocalDataSource> { BusinessNoticeLocalDataSourceImpl(get()) }
    single<EmployNoticeLocalDataSource> { EmployNoticeLocalDataSourceImpl(get()) }
    single<FavoriteNoticeLocalDataSource> { FavoriteNoticeLocalDataSourceImpl(get()) }

    single<BachelorNoticeDao> { get<NoticeDatabase>().bachelorNoticeDao() }
    single<GeneralNoticeDao> { get<NoticeDatabase>().generalNoticeDao() }
    single<BusinessNoticeDao> { get<NoticeDatabase>().businessNoticeDao() }
    single<EmployNoticeDao> { get<NoticeDatabase>().employsNoticeDao() }
    single<FavoriteNoticeDao> { get<NoticeDatabase>().favoriteNoticeDao() }

    single<NoticeDatabase> {
        Room.databaseBuilder(
            get(),
            NoticeDatabase::class.java, "Notice.db"
        ).build()
    }
}
package com.mtjin.cnunoticeapp.module

import com.mtjin.cnunoticeapp.data.bachelor.source.remote.BachelorNoticeRemoteDataSource
import com.mtjin.cnunoticeapp.data.bachelor.source.remote.BachelorNoticeRemoteDataSourceImpl
import com.mtjin.cnunoticeapp.data.business.source.remote.BusinessNoticeRemoteDataSource
import com.mtjin.cnunoticeapp.data.business.source.remote.BusinessNoticeRemoteDataSourceImpl
import com.mtjin.cnunoticeapp.data.employ.source.remote.EmployNoticeRemoteDataSource
import com.mtjin.cnunoticeapp.data.employ.source.remote.EmployNoticeRemoteDataSourceImpl
import com.mtjin.cnunoticeapp.data.general.source.remote.GeneralNoticeRemoteDataSource
import com.mtjin.cnunoticeapp.data.general.source.remote.GeneralNoticeRemoteDataSourceImpl
import org.koin.core.module.Module
import org.koin.dsl.module

val remoteDataModule: Module = module {
    single<BachelorNoticeRemoteDataSource> { BachelorNoticeRemoteDataSourceImpl() }
    single<GeneralNoticeRemoteDataSource> { GeneralNoticeRemoteDataSourceImpl() }
    single<BusinessNoticeRemoteDataSource> { BusinessNoticeRemoteDataSourceImpl() }
    single<EmployNoticeRemoteDataSource> { EmployNoticeRemoteDataSourceImpl() }
}
package com.mtjin.cnunoticeapp.module

import com.mtjin.cnunoticeapp.data.bachelor.source.BachelorNoticeRepository
import com.mtjin.cnunoticeapp.data.bachelor.source.BachelorNoticeRepositoryImpl
import com.mtjin.cnunoticeapp.data.business.source.BusinessNoticeRepository
import com.mtjin.cnunoticeapp.data.business.source.BusinessNoticeRepositoryImpl
import com.mtjin.cnunoticeapp.data.employ.source.EmployNoticeRepository
import com.mtjin.cnunoticeapp.data.employ.source.EmployNoticeRepositoryImpl
import com.mtjin.cnunoticeapp.data.favorite.source.FavoriteNoticeRepository
import com.mtjin.cnunoticeapp.data.favorite.source.FavoriteNoticeRepositoryImpl
import com.mtjin.cnunoticeapp.data.general.source.GeneralNoticeRepository
import com.mtjin.cnunoticeapp.data.general.source.GeneralNoticeRepositoryImpl
import org.koin.core.module.Module
import org.koin.dsl.module

val repositoryModule: Module = module {
    single<BachelorNoticeRepository> { BachelorNoticeRepositoryImpl(get(), get()) }
    single<GeneralNoticeRepository> { GeneralNoticeRepositoryImpl(get(), get()) }
    single<BusinessNoticeRepository> { BusinessNoticeRepositoryImpl(get(), get()) }
    single<EmployNoticeRepository> { EmployNoticeRepositoryImpl(get(), get()) }
    single<FavoriteNoticeRepository> { FavoriteNoticeRepositoryImpl(get()) }
}
package com.mtjin.cnunoticeapp.module

import com.mtjin.cnunoticeapp.views.bachelor.BachelorNoticeViewModel
import com.mtjin.cnunoticeapp.views.business.BusinessNoticeViewModel
import com.mtjin.cnunoticeapp.views.employ.EmployNoticeViewModel
import com.mtjin.cnunoticeapp.views.favorite.FavoriteViewModel
import com.mtjin.cnunoticeapp.views.general.GeneralNoticeViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.module.Module
import org.koin.dsl.module

val viewModelModule: Module = module {
    viewModel { BachelorNoticeViewModel(get()) }
    viewModel { GeneralNoticeViewModel(get()) }
    viewModel { BusinessNoticeViewModel(get()) }
    viewModel { EmployNoticeViewModel(get()) }
    viewModel { FavoriteViewModel(get()) }
}

 

 

 


 

3. Utils 클래스들을 정의 합니다. (데이터바인딩어댑터 , 무한스크롤 클래스, 네트워크 상태확인을 넣어줬습니다.)

 

[NetworkManager]

package com.mtjin.cnunoticeapp.utils

import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build

class NetworkManager(private val context: Context) {
    fun checkNetworkState(): Boolean {
        val connectivityManager =
            context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val nw = connectivityManager.activeNetwork ?: return false
            val actNw = connectivityManager.getNetworkCapabilities(nw) ?: return false
            return when {
                actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
                actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
                else -> false
            }
        } else {
            val nwInfo = connectivityManager.activeNetworkInfo ?: return false
            return nwInfo.isConnected
        }
    }
}

 

[무한스크롤]

package com.mtjin.cnunoticeapp.utils

import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager


abstract class EndlessRecyclerViewScrollListener : RecyclerView.OnScrollListener {
    // The minimum amount of items to have below your current scroll position
    // before loading more.
    private var visibleThreshold = 5

    // The current offset index of data you have loaded
    private var currentPage = 0

    // The total number of items in the dataset after the last load
    private var previousTotalItemCount = 0

    // True if we are still waiting for the last set of data to load.
    private var loading = true

    // Sets the starting page index
    private val startingPageIndex = 0
    var mLayoutManager: RecyclerView.LayoutManager

    constructor(layoutManager: LinearLayoutManager) {
        mLayoutManager = layoutManager
    }

    constructor(layoutManager: GridLayoutManager) {
        mLayoutManager = layoutManager
        visibleThreshold *= layoutManager.spanCount
    }

    constructor(layoutManager: StaggeredGridLayoutManager) {
        mLayoutManager = layoutManager
        visibleThreshold *= layoutManager.spanCount
    }

    private fun getLastVisibleItem(lastVisibleItemPositions: IntArray): Int {
        var maxSize = 0
        for (i in lastVisibleItemPositions.indices) {
            if (i == 0) {
                maxSize = lastVisibleItemPositions[i]
            } else if (lastVisibleItemPositions[i] > maxSize) {
                maxSize = lastVisibleItemPositions[i]
            }
        }
        return maxSize
    }

    // This happens many times a second during a scroll, so be wary of the code you place here.
    // We are given a few useful parameters to help us work out if we need to load some more data,
    // but first we check if we are waiting for the previous load to finish.
    override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
        var lastVisibleItemPosition = 0
        val totalItemCount = mLayoutManager.itemCount
        if (mLayoutManager is StaggeredGridLayoutManager) {
            val lastVisibleItemPositions =
                (mLayoutManager as StaggeredGridLayoutManager).findLastVisibleItemPositions(null)
            // get maximum element within the list
            lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions)
        } else if (mLayoutManager is GridLayoutManager) {
            lastVisibleItemPosition =
                (mLayoutManager as GridLayoutManager).findLastVisibleItemPosition()
        } else if (mLayoutManager is LinearLayoutManager) {
            lastVisibleItemPosition =
                (mLayoutManager as LinearLayoutManager).findLastVisibleItemPosition()
        }

        // If the total item count is zero and the previous isn't, assume the
        // list is invalidated and should be reset back to initial state
        if (totalItemCount < previousTotalItemCount) {
            currentPage = startingPageIndex
            previousTotalItemCount = totalItemCount
            if (totalItemCount == 0) {
                loading = true
            }
        }
        // If it’s still loading, we check to see if the dataset count has
        // changed, if so we conclude it has finished loading and update the current page
        // number and total item count.
        if (loading && totalItemCount > previousTotalItemCount) {
            loading = false
            previousTotalItemCount = totalItemCount
        }

        // If it isn’t currently loading, we check to see if we have breached
        // the visibleThreshold and need to reload more data.
        // If we do need to reload some more data, we execute onLoadMore to fetch the data.
        // threshold should reflect how many total columns there are too
        if (!loading && lastVisibleItemPosition + visibleThreshold > totalItemCount) {
            currentPage++
            onLoadMore(currentPage, totalItemCount, view)
            loading = true
        }
    }

    // Call this method whenever performing new searches
    fun resetState() {
        currentPage = startingPageIndex
        previousTotalItemCount = 0
        loading = true
    }

    // Defines the process for actually loading more data based on page
    abstract fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?)
}

 

 

[데이터바인딩 어댑터]

보통 사용하는 클래스 별로 나눠서 파일을 만들어주지만 규모가 큰 프로젝트가 아니므로 묶어주었습니다.

리사이클러뷰에 아이템을 세팅하거나 무한스크롤관련 로직을 바인딩어댑터로 처리했습니다.

package com.mtjin.cnunoticeapp.utils

import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.mtjin.cnunoticeapp.data.bachelor.BachelorNotice
import com.mtjin.cnunoticeapp.data.business.BusinessNotice
import com.mtjin.cnunoticeapp.data.employ.EmployNotice
import com.mtjin.cnunoticeapp.data.favorite.FavoriteNotice
import com.mtjin.cnunoticeapp.data.general.GeneralNotice
import com.mtjin.cnunoticeapp.views.bachelor.BachelorAdapter
import com.mtjin.cnunoticeapp.views.bachelor.BachelorNoticeViewModel
import com.mtjin.cnunoticeapp.views.business.BusinessAdapter
import com.mtjin.cnunoticeapp.views.business.BusinessNoticeViewModel
import com.mtjin.cnunoticeapp.views.employ.EmployAdapter
import com.mtjin.cnunoticeapp.views.employ.EmployNoticeViewModel
import com.mtjin.cnunoticeapp.views.employ.FavoriteAdapter
import com.mtjin.cnunoticeapp.views.general.GeneralAdapter
import com.mtjin.cnunoticeapp.views.general.GeneralNoticeViewModel

@BindingAdapter("setBachelorItems")
fun RecyclerView.setBachelorAdapterItems(items: List<BachelorNotice>?) {
    with((adapter as BachelorAdapter)) {
        this.clear()
        items?.let { this.addItems(it) }
    }
}

@BindingAdapter("setGeneralItems")
fun RecyclerView.setGeneralAdapterItems(items: List<GeneralNotice>?) {
    with((adapter as GeneralAdapter)) {
        this.clear()
        items?.let { this.addItems(it) }
    }
}

@BindingAdapter("setBusinessItems")
fun RecyclerView.setBusinessAdapterItems(items: List<BusinessNotice>?) {
    with((adapter as BusinessAdapter)) {
        this.clear()
        items?.let { this.addItems(it) }
    }
}

@BindingAdapter("setEmployItems")
fun RecyclerView.setEmployAdapterItems(items: List<EmployNotice>?) {
    with((adapter as EmployAdapter)) {
        this.clear()
        items?.let { this.addItems(it) }
    }
}

@BindingAdapter("setFavoriteItems")
fun RecyclerView.setFavoriteAdapterItems(items: List<FavoriteNotice>?) {
    with((adapter as FavoriteAdapter)) {
        this.clear()
        items?.let { this.addItems(it) }
    }
}

@BindingAdapter("bachelorEndlessScroll")
fun RecyclerView.setBachelorEndlessScroll(
    viewModel: BachelorNoticeViewModel
) {
    val scrollListener =
        object : EndlessRecyclerViewScrollListener(layoutManager as LinearLayoutManager) {
            override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
                viewModel.requestMoreNotice(totalItemsCount + 1)
            }
        }
    this.addOnScrollListener(scrollListener)
}

@BindingAdapter("generalEndlessScroll")
fun RecyclerView.setGeneralEndlessScroll(
    viewModel: GeneralNoticeViewModel
) {
    val scrollListener =
        object : EndlessRecyclerViewScrollListener(layoutManager as LinearLayoutManager) {
            override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
                viewModel.requestMoreNotice(totalItemsCount + 1)
            }
        }
    this.addOnScrollListener(scrollListener)
}

@BindingAdapter("businessEndlessScroll")
fun RecyclerView.setBusinessEndlessScroll(
    viewModel: BusinessNoticeViewModel
) {
    val scrollListener =
        object : EndlessRecyclerViewScrollListener(layoutManager as LinearLayoutManager) {
            override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
                viewModel.requestMoreNotice(totalItemsCount + 1)
            }
        }
    this.addOnScrollListener(scrollListener)
}

@BindingAdapter("employEndlessScroll")
fun RecyclerView.setEmployEndlessScroll(
    viewModel: EmployNoticeViewModel
) {
    val scrollListener =
        object : EndlessRecyclerViewScrollListener(layoutManager as LinearLayoutManager) {
            override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
                viewModel.requestMoreNotice(totalItemsCount + 1)
            }
        }
    this.addOnScrollListener(scrollListener)
}

 

 


4. 뷰를 정의 해줍니다.

MainActivity와 BachelorFragment 만 예시로 보겠습니다.

 

[MainActivity]

Jeptack bottom navigation 을 사용했습니다. 밑 사이트에 설명이 잘되어있습니다!

https://medium.com/@maryangmin/navigation-components-in-android-jetpack-1-introduction-e38442f70f

 

Navigation Components in Android Jetpack (1) — Introduction

Google이 Google I/O 2018에서 Jetpack을 발표하였습니다. Jetpack은 좋은 안드로이드 앱을 더 쉽게 만들 수 있도록 구글에서 지원하는 라이브러리 패키지를 뜻합니다. Jetpack에는 많은 라이브러리가 포함��

medium.com

package com.mtjin.cnunoticeapp.views.main

import android.os.Bundle
import androidx.navigation.findNavController
import androidx.navigation.ui.setupWithNavController
import com.google.firebase.analytics.FirebaseAnalytics
import com.mtjin.cnunoticeapp.R
import com.mtjin.cnunoticeapp.base.BaseActivity
import com.mtjin.cnunoticeapp.databinding.ActivityMainBinding

class MainActivity : BaseActivity<ActivityMainBinding>(R.layout.activity_main) {
    private var mFirebaseAnalytics: FirebaseAnalytics? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mFirebaseAnalytics = FirebaseAnalytics.getInstance(this)
        initNavigation()
    }

    private fun initNavigation() {
        val navController = findNavController(R.id.main_nav_host)
        binding.mainBottomNavigation.setupWithNavController(navController)
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".views.MainActivity">

        <FrameLayout
            android:id="@+id/main_fl_container"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintBottom_toTopOf="@id/main_bottom_navigation"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintEnd_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <fragment
                android:id="@+id/main_nav_host"
                android:name="androidx.navigation.fragment.NavHostFragment"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:defaultNavHost="true"
                app:navGraph="@navigation/bottom_nav_graph" />
        </FrameLayout>


        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/main_bottom_navigation"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/colorWhiteBlue"
            app:itemIconTint="@color/colorWhite"
            app:itemTextColor="@color/colorWhite"
            app:layout_behavior="tech.thdev.app.view.BottomNavigationBehavior"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/main_fl_container"
            app:menu="@menu/bottom_menu" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

 

 

[BachelorFragment]

뷰들을 초기화하는 로직이 들어가 있습니다. 그리고 리사이클러뷰의 클릭 이벤트를 받아서 처리해줍니다.

xml 에서는 로딩이나 아이템 세팅 같은 것을 데이터바인딩 처리를 해주었습니다.

package com.mtjin.cnunoticeapp.views.bachelor

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import com.mtjin.cnunoticeapp.R
import com.mtjin.cnunoticeapp.base.BaseFragment
import com.mtjin.cnunoticeapp.constants.EXTRA_NOTICE_SAVE
import com.mtjin.cnunoticeapp.constants.TAG_DIALOG_EVENT
import com.mtjin.cnunoticeapp.data.favorite.FavoriteNotice
import com.mtjin.cnunoticeapp.databinding.FragmentBachelorBinding
import com.mtjin.cnunoticeapp.utils.NetworkManager
import com.mtjin.cnunoticeapp.views.dialog.DialogAddFragment
import org.koin.androidx.viewmodel.ext.android.viewModel

class BachelorNoticeFragment :
    BaseFragment<FragmentBachelorBinding, BachelorNoticeViewModel>(R.layout.fragment_bachelor) {
    private lateinit var noticeAdapter: BachelorAdapter
    override val viewModel: BachelorNoticeViewModel by viewModel()

    override fun init() {
        binding.vm = viewModel
        initAdapter()
        val networkManager: NetworkManager? = context?.let { NetworkManager(it) }
        if (!networkManager?.checkNetworkState()!!) {
            showToast(getString(R.string.network_err_toast))
        }
        viewModel.requestNotice()
    }

    private fun initAdapter() {
        noticeAdapter = BachelorAdapter(itemClick = { item ->
            Intent(
                Intent.ACTION_VIEW,
                Uri.parse(item.link)
            ).run(this::startActivity)
        },
            numClick = {
                val bundle = Bundle()
                bundle.putParcelable(EXTRA_NOTICE_SAVE, FavoriteNotice(it.num, it.title, it.link))
                val dialog: DialogAddFragment = DialogAddFragment().getInstance()
                dialog.arguments = bundle
                activity?.supportFragmentManager?.let { fragmentManager ->
                    dialog.show(
                        fragmentManager,
                        TAG_DIALOG_EVENT
                    )
                }
            })
        binding.rvBachelors.adapter = noticeAdapter
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:bind="http://schemas.android.com/tools">

    <data>

        <import type="android.view.View" />

        <variable
            name="vm"
            type="com.mtjin.cnunoticeapp.views.bachelor.BachelorNoticeViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_bachelors"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            android:orientation="vertical"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            bind:bachelorEndlessScroll="@{vm}"
            bind:setBachelorItems="@{vm.noticeList}" />

        <ProgressBar
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="@{vm.isLoading() ? View.VISIBLE : View.GONE}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

 

[BachelorAdapter]

어댑터 또한 데이터바인딩 처리를 해주었습니다. 

클릭리스너는 고차함수를 사용해 프래그먼트 쪽에서 처리하게 해줍니다.

package com.mtjin.cnunoticeapp.views.bachelor

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import com.mtjin.cnunoticeapp.R
import com.mtjin.cnunoticeapp.data.bachelor.BachelorNotice
import com.mtjin.cnunoticeapp.databinding.ItemBachelorBinding

class BachelorAdapter(
    private val itemClick: (BachelorNotice) -> Unit,
    private val numClick: (BachelorNotice) -> Unit
) :
    RecyclerView.Adapter<BachelorAdapter.ViewHolder>() {

    private val items: ArrayList<BachelorNotice> = ArrayList()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BachelorAdapter.ViewHolder {
        val binding: ItemBachelorBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.item_bachelor,
            parent,
            false
        )
        val viewHolder = ViewHolder(binding)
        binding.root.setOnClickListener {
            itemClick(items[viewHolder.adapterPosition])
        }
        binding.bachelorTvNum.setOnClickListener {
            numClick(items[viewHolder.adapterPosition])
        }
        return viewHolder
    }

    override fun getItemCount(): Int = items.size

    override fun onBindViewHolder(holder: BachelorAdapter.ViewHolder, position: Int) {
        items[position].let {
            holder.bind(it)
        }
    }

    class ViewHolder(private val binding: ItemBachelorBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(item: BachelorNotice) {
            binding.item = item
            binding.executePendingBindings()
        }
    }

    fun addItems(items: List<BachelorNotice>) {
        this.items.addAll(items)
        notifyDataSetChanged()
    }

    fun clear() {
        this.items.clear()
        notifyDataSetChanged()
    }

}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="item"
            type="com.mtjin.cnunoticeapp.data.bachelor.BachelorNotice" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="50dp">

        <TextView
            android:id="@+id/bachelor_tv_num"
            android:layout_width="60dp"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="@{item.num}"
            android:textColor="@color/colorBlack"
            android:textSize="12sp"
            android:textStyle="bold"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <View
            android:layout_width="1dp"
            android:layout_height="match_parent"
            android:background="@color/colorBlack"
            app:layout_constraintEnd_toStartOf="@id/bachelor_tv_content"
            app:layout_constraintStart_toEndOf="@id/bachelor_tv_num"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/bachelor_tv_content"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:ellipsize="end"
            android:maxLines="2"
            android:padding="8dp"
            android:text="@{item.title}"
            android:textSize="12sp"
            app:layout_constraintStart_toEndOf="@+id/bachelor_tv_num"
            app:layout_constraintTop_toTopOf="parent" />

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="@color/colorBlack"
            android:padding="4dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toBottomOf="@id/bachelor_tv_num" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 


 

5. ViewModel 을 정의 해주도록 합니다.

 

[BacherlorNoticeViweModel]

Repository를 매개변수로 받습니다. (Koin을 통해 가져올 것 입니다.)

LiveData 를 사용해줍니다. (데이터의 상태를 Observe 해서 로직을 구현하기 위해)

BaseViewModel() 에  있는 compositeDisposable 을 사용하여 데이터를 가져오는 로직을 시작하도록 합니다.

그냥 가져오기와 페이징을 통해 가져오는 두가지 함수가 있습니다.

package com.mtjin.cnunoticeapp.views.bachelor

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.mtjin.cnunoticeapp.base.BaseViewModel
import com.mtjin.cnunoticeapp.data.bachelor.BachelorNotice
import com.mtjin.cnunoticeapp.data.bachelor.source.BachelorNoticeRepository
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers

class BachelorNoticeViewModel(private val bachelorRepository: BachelorNoticeRepository) :
    BaseViewModel() {
    private val _noticeList = MutableLiveData<ArrayList<BachelorNotice>>()

    val noticeList: LiveData<ArrayList<BachelorNotice>> get() = _noticeList

    fun requestNotice() {
        compositeDisposable.add(
            bachelorRepository.requestNotice()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { showProgress()}
                .doAfterTerminate { hideProgress()}
                .subscribe({
                    _noticeList.value = it as ArrayList<BachelorNotice>?
                }, {
                    Log.d(TAG, "" + it)
                })
        )
    }

    fun requestMoreNotice(offset: Int) {
        compositeDisposable.add(
            bachelorRepository.requestMoreNotice(offset)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { showProgress() }
                .doAfterTerminate { hideProgress() }
                .subscribe({ notices ->
                    val pagingNoticeList = _noticeList.value
                    pagingNoticeList?.addAll(notices)
                    _noticeList.value = pagingNoticeList
                }, {
                    Log.d(TAG, "" + it)
                })
        )
    }

    companion object {
        const val TAG = "BachelorNoticeViewModel"
    }
}

 

 

 


 

 

6.  DataSource(Model) 을 만들어줍니다. 먼저  Repository 를 살펴보도록 하겠습니다. 

인터페이스와 Impl class 로 나눠 구현해주었습니다.

인터넷 연결이 안된 경우는 Local 에서 데이터를 불러오도록 하고

[캐싱전략] 만약 인터넷 연결이 된 경우에는 Local 과 Remote 에서 동시에 불러옵니다. (캐싱된 데이터가 외부에서 불러오는 것보다 빠르므로 더 빠르게 데이터를 보게 해줄 수 있습니다.)

Remote에서 불러온 데이터는 Local DB 에 Insert 해주도록 합니다.

 

[BachelorNoticeRepository]

package com.mtjin.cnunoticeapp.data.bachelor.source

import com.mtjin.cnunoticeapp.data.bachelor.BachelorNotice
import io.reactivex.Flowable
import io.reactivex.Single

interface BachelorNoticeRepository {
    fun requestNotice(): Flowable<List<BachelorNotice>>
    fun requestMoreNotice(offset: Int): Single<List<BachelorNotice>>
}
package com.mtjin.cnunoticeapp.data.bachelor.source

import android.util.Log
import com.mtjin.cnunoticeapp.data.bachelor.BachelorNotice
import com.mtjin.cnunoticeapp.data.bachelor.source.local.BachelorNoticeLocalDataSource
import com.mtjin.cnunoticeapp.data.bachelor.source.remote.BachelorNoticeRemoteDataSource
import io.reactivex.Flowable
import io.reactivex.Single

class BachelorNoticeRepositoryImpl(
    private val bachelorNoticeRemoteDataSource: BachelorNoticeRemoteDataSource,
    private val bachelorNoticeLocalDataSource: BachelorNoticeLocalDataSource
) : BachelorNoticeRepository {
    override fun requestNotice(): Flowable<List<BachelorNotice>> {
        return bachelorNoticeLocalDataSource.getNotices()
            .onErrorReturn { listOf() }
            .flatMapPublisher { cachedMovies ->
                if (cachedMovies.isEmpty()) {
                    requestBachelorNotice()
                        .toFlowable()
                        .onErrorReturn {
                            listOf()
                        }
                } else {
                    val local = Single.just(cachedMovies)
                    val remote = requestBachelorNotice()
                        .onErrorResumeNext {
                            local
                        }
                    Single.concat(local, remote)
                }
            }
    }

    private fun requestBachelorNotice(): Single<List<BachelorNotice>> {
        return bachelorNoticeRemoteDataSource.requestNotice()
            .flatMap {
                bachelorNoticeLocalDataSource.insertNotice(it)
                    .andThen(Single.just(it))
            }
    }

    override fun requestMoreNotice(offset: Int): Single<List<BachelorNotice>> {
        return bachelorNoticeRemoteDataSource.requestMoreNotice(offset)
    }
}

 

 

 


 

 

 

7. LocalDataSource 를 구현합니다. Room 을 사용하며 ROOM DB, Dao 와 엔티티 모델 클래스도 구현해야합니다.

 

[Room DB]

package com.mtjin.cnunoticeapp.database

import androidx.room.Database
import androidx.room.RoomDatabase
import com.mtjin.cnunoticeapp.data.bachelor.BachelorNotice
import com.mtjin.cnunoticeapp.data.bachelor.source.local.BachelorNoticeDao
import com.mtjin.cnunoticeapp.data.business.BusinessNotice
import com.mtjin.cnunoticeapp.data.business.source.local.BusinessNoticeDao
import com.mtjin.cnunoticeapp.data.employ.EmployNotice
import com.mtjin.cnunoticeapp.data.employ.source.local.EmployNoticeDao
import com.mtjin.cnunoticeapp.data.favorite.FavoriteNotice
import com.mtjin.cnunoticeapp.data.favorite.source.local.FavoriteNoticeDao
import com.mtjin.cnunoticeapp.data.general.GeneralNotice
import com.mtjin.cnunoticeapp.data.general.source.local.GeneralNoticeDao

@Database(
    entities = [BachelorNotice::class, GeneralNotice::class, BusinessNotice::class, EmployNotice::class, FavoriteNotice::class],
    version = 1,
    exportSchema = false
)
abstract class NoticeDatabase : RoomDatabase() {
    abstract fun bachelorNoticeDao(): BachelorNoticeDao
    abstract fun generalNoticeDao(): GeneralNoticeDao
    abstract fun businessNoticeDao(): BusinessNoticeDao
    abstract fun employsNoticeDao(): EmployNoticeDao
    abstract fun favoriteNoticeDao(): FavoriteNoticeDao
}

참고로 지금까지 모든 것들이 그러하지만 생성은 Koin Module 에 다음과 같이 정의해놨습니다.  

single<NoticeDatabase> {
        Room.databaseBuilder(
            get(),
            NoticeDatabase::class.java, "Notice.db"
        ).build()
    }

 

 

[Entitiy Model class]

테이블 네임과 컬럼명 프라이머리 키 등을 정의해 줄 수 있습니다.

package com.mtjin.cnunoticeapp.data.bachelor

import androidx.room.Entity
import androidx.room.PrimaryKey

/*
*  num 값을 주요키 id 로 사용할려 했으나 공지 라는 이름의 아이디 떄문에 중복값이 발생하여 사용불가하다.
* */
@Entity(tableName = "bachelor")
data class BachelorNotice(
    @PrimaryKey
    val num: String,
    val title: String,
    var link: String
)

 

[BachelorDao]

SQL 을 정의 해줍니다.

package com.mtjin.cnunoticeapp.data.bachelor.source.local

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.mtjin.cnunoticeapp.data.bachelor.BachelorNotice
import io.reactivex.Completable
import io.reactivex.Single

@Dao
interface BachelorNoticeDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertNotice(notice: List<BachelorNotice>): Completable

    @Query("SELECT * FROM bachelor  ORDER BY 1 DESC")
    fun getNotices(): Single<List<BachelorNotice>>
}

 

 

[BachelorLocalDataSource]

로컬 데이터 소스 족의 로직을 처리합니다. 삽입과 데이터를 가져오는 것을 구현했습니다.

package com.mtjin.cnunoticeapp.data.bachelor.source.local

import com.mtjin.cnunoticeapp.data.bachelor.BachelorNotice
import io.reactivex.Completable
import io.reactivex.Single

interface BachelorNoticeLocalDataSource {
    fun insertNotice(notice: List<BachelorNotice>) : Completable

    fun getNotices(): Single<List<BachelorNotice>>
}
package com.mtjin.cnunoticeapp.data.bachelor.source.local

import com.mtjin.cnunoticeapp.data.bachelor.BachelorNotice
import io.reactivex.Completable
import io.reactivex.Single

class BachelorNoticeLocalDataSourceImpl(private val bachelorNoticeDao: BachelorNoticeDao) :
    BachelorNoticeLocalDataSource {
    override fun insertNotice(notice: List<BachelorNotice>): Completable {
        return bachelorNoticeDao.insertNotice(notice)
    }

    override fun getNotices(): Single<List<BachelorNotice>> {
        return bachelorNoticeDao.getNotices()
    }
}

 

 


8. RemoteDataSource 를 구현합니다. 네트워크 통신을 통해 외부 API 데이터를 가져오도록 합니다. 저는 이 프로젝트에서는 크롤링을 사용했으므로 Retrofit2 가 아닌 Jsoup을 통해 학교 웹 데이터를 크롤링 해왔습니다.

크롤링한 데이터를 Observable 형태로 만들어 레포지토리에 전달해주도록 했습니다.

package com.mtjin.cnunoticeapp.data.bachelor.source.remote

import com.mtjin.cnunoticeapp.data.bachelor.BachelorNotice
import io.reactivex.Single

interface BachelorNoticeRemoteDataSource {
    fun requestNotice(): Single<List<BachelorNotice>>
    fun requestMoreNotice(offset: Int): Single<List<BachelorNotice>>
}
package com.mtjin.cnunoticeapp.data.bachelor.source.remote

import com.mtjin.cnunoticeapp.data.bachelor.BachelorNotice
import io.reactivex.Observable
import io.reactivex.Single
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.select.Elements

class BachelorNoticeRemoteDataSourceImpl : BachelorNoticeRemoteDataSource {
    override fun requestNotice(): Single<List<BachelorNotice>> {
        return Single.fromObservable(
            Observable.create {
                val bachNoticeList: ArrayList<BachelorNotice> = ArrayList()
                val doc: Document =
                    Jsoup.connect("https://computer.cnu.ac.kr/computer/notice/bachelor.do")
                        .get() // Base Url
                val contentElements: Elements =
                    doc.select("div[class=b-title-box]").select("a") // title, link
                val idElements: Elements = doc.select("td[class=b-num-box]") // id값
                for ((i, elem) in contentElements.withIndex()) {
                    bachNoticeList.add(
                        BachelorNotice(
                            idElements[i].text(),
                            elem.text(),
                            "https://computer.cnu.ac.kr/computer/notice/bachelor.do" + elem.attr("href")
                        )
                    )
                }
                it.onNext(bachNoticeList)
                it.onComplete()
            }
        )
    }

    override fun requestMoreNotice(offset: Int): Single<List<BachelorNotice>> {
        return Single.fromObservable(
            Observable.create {
                val bachNoticeList: ArrayList<BachelorNotice> = ArrayList()
                val doc: Document =
                    Jsoup.connect("https://computer.cnu.ac.kr/computer/notice/bachelor.do?mode=list&&articleLimit=10&article.offset=$offset")
                        .get() // Base Url
                val contentElements: Elements =
                    doc.select("div[class=b-title-box]").select("a") // title, link
                val idElements: Elements = doc.select("td[class=b-num-box]") // id값
                for ((i, elem) in contentElements.withIndex()) {
                    if (idElements[i].text() != "공지") { // 공지는 매 페이지마다 있으므로 중복제거
                        bachNoticeList.add(
                            BachelorNotice(
                                idElements[i].text(),
                                elem.text(),
                                "https://computer.cnu.ac.kr/computer/notice/bachelor.do" + elem.attr(
                                    "href"
                                )
                            )
                        )
                    }
                }
                it.onNext(bachNoticeList)
                it.onComplete()
            }
        )
    }

}

 

 

 


P.S 다이어로그 프래그먼트입니다. 예, 아니오 로직의 간단한 프래그먼트입니다. (여러군데에서 재사용하며 두가지 로직을 갖고 있습니다. 상황에 따라 질문 Text 도 변경되게 해야했습니다.)

package com.mtjin.cnunoticeapp.views.dialog

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import com.mtjin.cnunoticeapp.R
import com.mtjin.cnunoticeapp.constants.EXTRA_NOTICE_DELETE
import com.mtjin.cnunoticeapp.constants.EXTRA_NOTICE_SAVE
import com.mtjin.cnunoticeapp.data.favorite.FavoriteNotice
import com.mtjin.cnunoticeapp.views.favorite.FavoriteViewModel
import kotlinx.android.synthetic.main.fragment_dialog_add.view.*
import org.koin.androidx.viewmodel.ext.android.viewModel

class DialogAddFragment : DialogFragment(), View.OnClickListener {
    private val viewModel: FavoriteViewModel by viewModel()
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_dialog_add, container, false)
        processBundle(view)
        return view
    }

    private fun processBundle(view: View) {
        val bundle = arguments
        val notice = bundle?.getParcelable<FavoriteNotice>(EXTRA_NOTICE_SAVE)
        when (bundle?.getString(EXTRA_NOTICE_DELETE, "")) {
            EXTRA_NOTICE_DELETE -> {
                view.dialog_tv_question.text = getString(R.string.dialog_favorite_delete_text)
                view.dialog_tv_yes.setOnClickListener {
                    notice?.let { notice ->
                        viewModel.delete(notice)
                        showToast(getString(R.string.delete_success_text))
                        dismiss()
                    }
                }
                view.dialog_tv_no.setOnClickListener {
                    dismiss()
                }
            }
            else -> {
                view.dialog_tv_yes.setOnClickListener {
                    notice?.let { notice ->
                        viewModel.insert(notice)
                        showToast(getString(R.string.add_success_text))
                        dismiss()
                    }
                }
                view.dialog_tv_no.setOnClickListener {
                    dismiss()
                }
            }
        }
    }

    fun showToast(msg: String) {
        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
    }

    fun getInstance(): DialogAddFragment {
        return DialogAddFragment()
    }

    override fun onClick(p0: View?) {
        dismiss()
    }

}

 

 

 


[결과]

 

 

 

 


 

완벽하진 않지만 나름대로 MVVM 아키텍처를 적용해서 만들어봤습니다. Jsoup은 처음 사용하는거라 원인 모를 에러가 많아서 그 부분에서 시간이 조금 걸렸던 것 같습니다. (HTML id 구조 이상하게 된 부분이 한 군데 있더라고요) 

 

 

처음 안드로이드 시작할 때 MVC 로 막 짜던 때보다 유지보수면에서도 좋은 것 같습니다.

 

 

 

댓글과 공감은 큰 힘이 됩니다. 감사합니다.!!!

728x90
Comments