관리 메뉴

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

[안드로이드] 안드로이드 아키텍처 스터디 총 정리(MVC,MVVM, AAC ViewModel, LiveData, Koin, Dagger2, RxJava2, Multi Module) Kotlin 본문

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

[안드로이드] 안드로이드 아키텍처 스터디 총 정리(MVC,MVVM, AAC ViewModel, LiveData, Koin, Dagger2, RxJava2, Multi Module) Kotlin

막무가내막내 2020. 3. 15. 21:48
728x90

 

 

 

[2021-04-13 업데이트]

 

 

참고사이트 : https://github.com/android/architecture-samples

 

android/architecture-samples

A collection of samples to discuss and showcase different architectural tools and patterns for Android apps. - android/architecture-samples

github.com

 

안드로이드 스터디를 했던 내용들을 정리합니다. 

스터디를 하면서 공식문서로 설명을 듣고 프로젝트에 적용하는식으로 진행했습니다.

안드로이드 개발자 공식문서가 잘 되있으므로 가장 먼저 참고하면서 공부합니다.

 

프로젝트 저장소 

https://github.com/mtjin/android-architecture-study-movieapp

 

mtjin/android-architecture-study-movieapp

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

github.com

 

 

 

[1주차]

주제: MVC구현

- 네이버 영화검색앱 기능구현 및 깃 코드리부와 Git Flow 경험

 

MVC로 처음 안드로이드를 접했을때처럼 구현을 했다.

기본적인 구현은 다음 레포를 참고합니다.

https://github.com/boostcourse-connect/boostcamp_3_Android

 

 

[리뷰]

https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/510

[원래는 검색화면만 있었는데 로그인화면과 스플래쉬 화면을 추가 구현했습니다.]

https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/520

 

 


[2주차]

 

 

주제: Model(DataSource, Repository) 추가

- model은 단순 data class가 아니다.

 

Model은 다음과 같은 구조를 가진다.

-Repository

--Remote Data source -> remote get, post (Retrofit2)

--Local Data source --> sharedpref, room(sqlite), file

서버와 로컬데이터에 접근하는 DataSource가 있고 이를 Repository 에서 기능을 통합적으로 제공해준다.

 

 

- 무한스크롤과 네트워크 상태를 확인하는 기능을 추가했다.

 

[리뷰]

https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/525

 

 


[3주차] 

 

 

주제 : MVP (Model View Presenter)

- 기존 View에서 유효성(정규식) 검사, 요청하는 로직 등을 구현했던것을 Presenter에서 처리하도록 한다.

- 위 그림처럼 View는 뷰의 I/O(입력받고 보여지는) 역할만 하게하고 Model은 Local 또는 Server와 통신하여 데이터를 저장하고 받는 역할만 하도록 한다. 나머지 로직들은 Presenter에서 하며 View와 Model을 이어주는 매개체 역할을 한다.

- 사람으로 치면 View는 눈 , Presenter는 뇌,(매개체), Model은 행동(손) 이라고 생각하면 이해하기 쉽다.

- 로직이 분리되어 협업과 유지보수면에서 좋다. 그러나 생성해야할 클래스가 많아진다는 단점이 있다. (보일러플레이트 코드 증가)

 

MVP 구현 방법은 요약하면 다음과 같다.

1. Contract 만들기
2. Contract 안에 View, Presenter 만들기
3. MainPresenter 클래스 만들기
4. MainActivity 와 MainPresenter 가 Contract 에 있는 View 와 Presenter 를 impl 하기
5. MainActivity 와 MainPresenter 연결하기

 

 

실제 구현한 코드의 예이다.

[Contract Interface] 계약관계를 설계한다

package com.mtjin.androidarchitecturestudy.ui.search

import com.mtjin.androidarchitecturestudy.data.search.Movie

interface MovieSearchContract {

    interface View {
        fun showLoading()
        fun hideLoading()
        fun showToast(msg: String)
        fun showEmptyQuery()
        fun showWait()
        fun showNetworkError()
        fun showNoMovie()
        fun showLastPage()
        fun scrollResetState()
        fun searchMovieSuccess(movieList: List<Movie>)
        fun pagingMovieSuccess(movieList: List<Movie>)
    }

    interface Presenter {
        fun requestMovie(query: String)
        fun requestPagingMovie(query: String, offset: Int)
    }
}

 

[Presenter] 로직을 담당한다.(Contract.Presenter 함수를 구현해야함) Contract.View 를 매개변수로 받는다. view로 인터페이스에서 정의한 뷰 관련 동작만 시킬 수 있다.

package com.mtjin.androidarchitecturestudy.ui.search

import android.util.Log
import com.mtjin.androidarchitecturestudy.data.search.source.MovieRepository
import retrofit2.HttpException

class MovieSearchPresenter(
    private val view: MovieSearchContract.View,
    private val movieRepository: MovieRepository
) :
    MovieSearchContract.Presenter {

    override fun requestMovie(query: String) {
        if (query.isEmpty()) {
            view.showEmptyQuery()
        } else {
            view.showWait()
            view.showLoading()
            view.scrollResetState()
            movieRepository.getSearchMovies(query,
                success = {
                    if (it.isEmpty()) {
                        view.showNoMovie()
                    } else {
                        view.searchMovieSuccess(it)
                    }
                    view.hideLoading()
                },
                fail = {
                    Log.d(TAG, it.toString())
                    when (it) {
                        is HttpException -> view.showNetworkError()
                        else -> view.showToast(it.message.toString())
                    }
                    view.hideLoading()
                })
        }
    }

    override fun requestPagingMovie(query: String, offset: Int) {
        view.showLoading()
        movieRepository.getPagingMovies(query, offset,
            success = {
                if (it.isEmpty()) {
                    view.showLastPage()
                } else {
                    view.pagingMovieSuccess(it)
                }
                view.hideLoading()
            },
            fail = {
                Log.d(TAG, it.toString())
                when (it) {
                    is HttpException -> view.showNetworkError()
                    else -> view.showToast(it.message.toString())
                }
                view.hideLoading()
            })
    }

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

 

[View Activity]  Contract.View 관련 함수를 구현해야한다. 그리고 Pesenter 생성자에는 자신을(this) 넘겨준다.

package com.mtjin.androidarchitecturestudy.ui.search

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.mtjin.androidarchitecturestudy.R
import com.mtjin.androidarchitecturestudy.data.search.Movie
import com.mtjin.androidarchitecturestudy.utils.MyApplication


class MovieSearchActivity : AppCompatActivity(), MovieSearchContract.View {

    private lateinit var presenter: MovieSearchContract.Presenter
    private lateinit var etInput: EditText
    private lateinit var btnSearch: Button
    private lateinit var rvMovies: RecyclerView
    private lateinit var pbLoading: ProgressBar
    private lateinit var movieAdapter: MovieAdapter
    private lateinit var myApplication: MyApplication
    private lateinit var query: String
    private lateinit var scrollListener: EndlessRecyclerViewScrollListener

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_movie_search)

        initView()
        initAdapter()
        initListener()
        presenter = MovieSearchPresenter(this, myApplication.movieRepository)
    }

    private fun initView() {
        myApplication = application as MyApplication
        etInput = findViewById(R.id.et_input)
        btnSearch = findViewById(R.id.btn_search)
        rvMovies = findViewById(R.id.rv_movies)
        pbLoading = findViewById(R.id.pb_loading)
    }

    private fun initAdapter() {
        movieAdapter = MovieAdapter()
        val linearLayoutManager = LinearLayoutManager(this)
        rvMovies.layoutManager = linearLayoutManager
        scrollListener = object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
            override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
                presenter.requestPagingMovie(query, totalItemsCount + 1)
            }
        }
        rvMovies.addOnScrollListener(scrollListener)
        rvMovies.adapter = movieAdapter
    }

    private fun initListener() {
        //어댑터 아이템 클릭리스너
        movieAdapter.setItemClickListener { movie ->
            Intent(Intent.ACTION_VIEW, Uri.parse(movie.link)).takeIf {
                it.resolveActivity(packageManager) != null
            }?.run(this::startActivity)
        }
        //검색버튼
        btnSearch.setOnClickListener {
            query = etInput.text.toString().trim()
            presenter.requestMovie(query)
        }
    }

    override fun showLoading() {
        pbLoading.visibility = View.VISIBLE
    }

    override fun hideLoading() {
        pbLoading.visibility = View.GONE
    }

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

    override fun showEmptyQuery() {
        Toast.makeText(this, getString(R.string.search_input_query_msg), Toast.LENGTH_SHORT).show()
    }

    override fun showWait() {
        Toast.makeText(this, getString(R.string.wait_toast), Toast.LENGTH_SHORT).show()
    }

    override fun showNetworkError() {
        Toast.makeText(this, getString(R.string.network_error_msg), Toast.LENGTH_SHORT).show()
    }

    override fun showNoMovie() {
        Toast.makeText(this, getString(R.string.no_movie_error_msg), Toast.LENGTH_SHORT).show()
    }

    override fun showLastPage() {
        Toast.makeText(this, getString(R.string.last_page_msg), Toast.LENGTH_SHORT).show()
    }

    override fun scrollResetState() {
        scrollListener.resetState()
    }

    override fun searchMovieSuccess(movieList: List<Movie>) {
        movieAdapter.clear()
        movieAdapter.setItems(movieList)
        Toast.makeText(this, getString(R.string.load_movie_success_msg), Toast.LENGTH_SHORT).show()
    }

    override fun pagingMovieSuccess(movieList: List<Movie>) {
        movieAdapter.setItems(movieList)
        Toast.makeText(this, getString(R.string.load_movie_success_msg), Toast.LENGTH_SHORT).show()
    }


}

 

 

[예전에 썻던 글]

https://youngest-programming.tistory.com/111

 

[안드로이드] MVP 디자인패턴 간략정리

프로젝트에 MVC패턴만 사용하다가 최근 간단한 공부용 프로젝트를 통해 MVP 디자인 패턴을 적용해보고있다. 확실히 기존 MVC 구조보다 코드가 정리되는 느낌을 갖었다. MVP구조로 짜는 연습을 많이

youngest-programming.tistory.com

[참고할 것]

참고 : http://www.codeplayon.com/2019/07/android-mvp-model-view-presenter-example-and-how-to-work-it/

 

MVP pattern android example - Codeplayon

Android MVP (Model-View-Presenter) Example And How to Work it

www.codeplayon.com

https://github.com/android/architecture-samples/tree/todo-mvp-kotlin

 

android/architecture-samples

A collection of samples to discuss and showcase different architectural tools and patterns for Android apps. - android/architecture-samples

github.com

 

 

[리뷰]

https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/529

 

 

 


[4주차] 

주제 : 데이터바인딩 (Databinding)

코틀린 코드로 하던 작업을 xml 에서 처리해 줄 수 있게 해줍니다. 코틀린 아키텍처의 필수요소 입니다.

layouts and binding expressions 및 binding adapters 등을 적용해봤습니다. two-way databinding은 뷰의 값을 보존하면서 변경로직을 구현하기위해 자주 사용된다. (아직 LiveData와 mvvm 적용전이라 해당부분은 적용되지 않았습니다.)

 

-참고 : 데이터바인딩 완전하게 적용하기 전입니다-

 

어댑터에서 executePendingBindings() 을 사용하면 더 효과적입니다.

ex) Evaluates the pending bindings, updating any Views that have expressions bound to modified variables.

fun bind(movie: Movie) {
            binding.movie = movie
            binding.executePendingBindings()
}

 

build.gradle 에 다음을 추가해서 데이터바인딩이 가능하게 해야한다.

dataBinding {
        enabled = true
    }

현재 2021년 기준으로는 다음과 같이 변경되었다.

android {
    ...
    buildFeatures {
        dataBinding true
    }
}

 

 

 

적용한 예시다. 

 

[액티비티]

xml 이름이 activity_move_search.xml 이라면 다음과 같은 이름으로 바인딩 객체를 생성할 수 있다.

private lateinit var binding: ActivityMovieSearchBinding

기존의 setContentView()를 제거해주고 데이터바인딩을 사용해 뷰를 객체화 시키도록 한다

뒤에 나올 xml의 <data> 를 주입시킬 수도 있다.

private fun initDataBinding() {
        binding = DataBindingUtil.setContentView(this, R.layout.activity_movie_search)
        binding.search = this
    }

 

[XML]

<layout> 태그가 붙게되고 <data> 태그를 사용해 뷰나 모델객체, 프레젠터 등을 가져다 쓸 수도 있다.  

클릭리스너도 onClick으로 설정해 줄 수 있고 액티비티에서 하던 것을 xml에서 대신 해줄 수 있는게 많다.

<?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>

        <variable
            name="search"
            type="com.mtjin.androidarchitecturestudy.ui.search.MovieSearchActivity" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="8dp"
        tools:context=".ui.search.MovieSearchActivity">

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/guideline"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintGuide_percent="0.08" />

        <EditText
            android:id="@+id/et_input"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:hint="@string/movie_search_input_hint"
            android:inputType="text"
            app:layout_constraintBottom_toTopOf="@+id/guideline"
            app:layout_constraintEnd_toStartOf="@+id/btn_search"
            app:layout_constraintHorizontal_weight="8"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/btn_search"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:onClick="@{()->search.onSearchClick()}"
            android:text="@string/movie_search_text"
            app:layout_constraintBottom_toTopOf="@id/guideline"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_weight="2"
            app:layout_constraintStart_toEndOf="@+id/et_input"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_movies"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:clipToPadding="false"
            android:orientation="vertical"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/guideline" />

        <ProgressBar
            android:id="@+id/pb_loading"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:visibility="gone"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

 

[어댑터도 비슷하게 해준다.] -> 더 자세한 사항과 구현된 예제는 다음 링크를 참고 

https://youngest-programming.tistory.com/199

 

[코틀린] 리사이클러뷰 표본

코틀린으로 짠 리사이클러뷰 표본입니다. 기록용으로 작성했습니다. 어댑터 (클릭리스너를 어댑터 생성자로 받아도 됩니다. -> 생성자로 받는게 더 깔끔한것같습니다.) -> movieAdapter = MovieAdapter {

youngest-programming.tistory.com

 lateinit var binding: ItemMovieBinding
    private val items: ArrayList<Movie> = ArrayList()

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

        return viewHolder
    }

 

 

[참고할 것]

https://developer.android.com/topic/libraries/data-binding?hl=eng%EF%BB%BF

 

데이터 결합 라이브러리  |  Android 개발자  |  Android Developers

데이터 결합 라이브러리   Android Jetpack의 구성요소 데이터 결합 라이브러리는 프로그래매틱 방식이 아니라 선언적 형식으로 레이아웃의 UI 구성요소를 앱의 데이터 소스와 결합할 수 있는 지원 라이브러리입니다. 레이아웃은 흔히 UI 프레임워크 메서드를 호출하는 코드가 포함된 활동에서 정의됩니다. 예를 들어 아래 코드는 findViewById()를 호출하여 TextView 위젯을 찾아 viewModel 변수의 userName 속성에 결합합니다. K

developer.android.com

https://developer.android.com/topic/libraries/data-binding

 

데이터 결합 라이브러리  |  Android 개발자  |  Android Developers

데이터 결합 라이브러리   Android Jetpack의 구성요소 데이터 결합 라이브러리는 프로그래매틱 방식이 아니라 선언적 형식으로 레이아웃의 UI 구성요소를 앱의 데이터 소스와 결합할 수 있는 지원 라이브러리입니다. 레이아웃은 흔히 UI 프레임워크 메서드를 호출하는 코드가 포함된 활동에서 정의됩니다. 예를 들어 아래 코드는 findViewById()를 호출하여 TextView 위젯을 찾아 viewModel 변수의 userName 속성에 결합합니다. K

developer.android.com

 

[리뷰]

mvvm 적용 전입니다. mvp에서 데이터바인딩 적용

https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/543

 

 


[5주차]

주제 : MVVM 

귀엽게 잘 설명하신 것 같다. 출처: https://blog.yena.io/studynote/2019/03/16/Android-MVVM-AAC-1.html

지난주에 데이터바인딩이 적용된 MVP 코드에서 P -> VM 으로 바꾸는 작업을 합니다. (LiveData는 아직 사용하지 않습니다. AAC 적용 X ) 

ViewModel 클래스는 수명 주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계되었습니다. ViewModel 클래스를 사용하면 화면 회전과 같이 구성을 변경할 때도 데이터를 유지할 수 있습니다.(AAC ViewModel 사용시 회전시 데이터 유지 가능 뒤에 6주차에서 다룸)

 

MVP 와 MVVM의 여러 차이점 중에 하나는 MVP 에서는 뷰가 프레젠터가 명령를 내리면 그때 행동하는 것 처럼 수동적이다. 허나 MVVM에서는 뷰가 능동적이다. View는 스스로 ViewModel 객체의 어떤 데이터가 필요한지 직접 관찰한다. 그래서 MVVM 에서는 옵저버 패턴이 필수적으로 사용된다.

 

프로젝트 구현은 다음과 같이 했다. (예시) 

- 아직 AAC ViewModel 이 아니므로 ViewModel 을 상속받지는 않았고 LiveData 또한 적용 전 상태라 ObseravableField를 사용하였다.

 

[ViewModel]

class MovieSearchViewModel(private val movieRepository: MovieRepository) {

    var query: ObservableField<String> = ObservableField("")
    var movieList: ObservableField<ArrayList<Movie>> = ObservableField()
    var toastMsg: ObservableField<MessageSet> = ObservableField()
    var isLoading: ObservableBoolean = ObservableBoolean(false)
    private var currentQuery: String = ""

    fun requestMovie() {
        currentQuery = query.get().toString()
        if (currentQuery.isEmpty()) {
            toastMsg.set(MessageSet.EMPTY_QUERY)
        } else {
            isLoading.set(true)
            movieRepository.getSearchMovies(query.get().toString(),
                success = {
                    if (it.isEmpty()) {
                        toastMsg.set(MessageSet.NO_RESULT)
                    } else {
                        movieList.set(it as ArrayList<Movie>?)
                        toastMsg.set(MessageSet.SUCCESS)
                    }
                    isLoading.set(false)
                },
                fail = {
                    Log.d(TAG, it.toString())
                    when (it) {
                        is HttpException -> toastMsg.set(MessageSet.NETWORK_ERROR)
                        else -> toastMsg.set(MessageSet.NETWORK_ERROR)
                    }
                    isLoading.set(false)
                })
        }
    }

    fun requestPagingMovie(offset: Int) {
        isLoading.set(true)
        movieRepository.getPagingMovies(currentQuery, offset,
            success = {
                if (it.isEmpty()) {
                    toastMsg.set(MessageSet.LAST_PAGE)
                } else {
                    movieList.get()?.addAll(it)
                    movieList.notifyChange()
                    toastMsg.set(MessageSet.SUCCESS)
                }
                isLoading.set(false)
            },
            fail = {
                Log.d(TAG, it.toString())
                when (it) {
                    is HttpException -> toastMsg.set(MessageSet.NETWORK_ERROR)
                    else -> toastMsg.set(MessageSet.LAST_PAGE)
                }
                isLoading.set(false)
            })
    }

    enum class MessageSet {
        BASIC,
        LAST_PAGE,
        EMPTY_QUERY,
        NETWORK_ERROR,
        SUCCESS,
        NO_RESULT
    }

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

 

[Activity]

class MovieSearchActivity : BaseActivity() {

    private lateinit var binding: ActivityMovieSearchBinding
    private lateinit var movieAdapter: MovieAdapter
    private lateinit var myApplication: MyApplication
    private lateinit var viewModel: MovieSearchViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        inject()
        initViewModelCallback()
        initAdapter()
    }

    private fun inject() {
        myApplication = application as MyApplication
        binding = DataBindingUtil.setContentView(this, R.layout.activity_movie_search)
        viewModel = MovieSearchViewModel(myApplication.movieRepository)
        binding.vm = viewModel
    }

    private fun initAdapter() {
        movieAdapter = MovieAdapter { movie ->
            Intent(Intent.ACTION_VIEW, Uri.parse(movie.link)).takeIf {
                it.resolveActivity(packageManager) != null
            }?.run(this::startActivity)
        }
        binding.rvMovies.adapter = movieAdapter
    }

    private fun initViewModelCallback() {
        with(viewModel) {
            toastMsg.addOnPropertyChangedCallback(object :
                Observable.OnPropertyChangedCallback() {
                override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
                    when (toastMsg.get()) {
                        MovieSearchViewModel.MessageSet.LAST_PAGE -> showToast(getString(R.string.last_page_msg))
                        MovieSearchViewModel.MessageSet.EMPTY_QUERY -> showToast(getString(R.string.search_input_query_msg))
                        MovieSearchViewModel.MessageSet.NETWORK_ERROR -> showToast(getString(R.string.network_error_msg))
                        MovieSearchViewModel.MessageSet.SUCCESS -> showToast(getString(R.string.load_movie_success_msg))
                        MovieSearchViewModel.MessageSet.NO_RESULT -> showToast(getString(R.string.no_movie_error_msg))
                    }
                    toastMsg.set(MovieSearchViewModel.MessageSet.BASIC)
                }
            })
        }
    }
}

 

참고로 MyApplication은 DI(KOIN) 적용 전에 임의로 만든 것인데, 매번 매번 액티비티가 생성될 때마다 주입하는 것보다 Application객체에 생성해놓고 가져오는게 좋아서 그렇게 구현했다.

[Applicaton]

class MyApplication : Application() {
    private lateinit var networkManager: NetworkManager
    private lateinit var apiInterface: ApiInterface
    lateinit var movieRepository: MovieRepository
    private lateinit var movieRemoteDataSource: MovieRemoteDataSource
    private lateinit var movieLocalDataSource: MovieLocalDataSource
    private lateinit var movieDao: MovieDao

    override fun onCreate() {
        super.onCreate()
        inject()
    }

    private fun inject() {
        networkManager = NetworkManager(applicationContext)
        apiInterface = ApiClient.getApiClient().create(ApiInterface::class.java)
        movieDao = MovieDatabase.getInstance(this).movieDao()
        movieRemoteDataSource =
            MovieRemoteDataSourceImpl(
                apiInterface
            )
        movieLocalDataSource = MovieLocalDataSourceImpl(movieDao)
        movieRepository = MovieRepositoryImpl(movieRemoteDataSource, movieLocalDataSource, networkManager)
    }
}

 

[참고할 것]

https://developer.android.com/topic/libraries/data-binding/observability

 

식별 가능한 데이터 개체 작업  |  Android 개발자  |  Android Developers

식별 가능성은 개체가 데이터 변경에 관해 다른 개체에 알릴 수 있는 기능을 의미합니다. 데이터 결합 라이브러리를 통해 개체, 필드 또는 컬렉션을 식별 가능하게 만들 수 있습니다. 간단한 기존 개체를 데이터 결합에 사용할 수는 있지만 개체를 수정해도 UI가 자동으로 업데이트되지 않습니다. 데이터 결합을 사용하면 데이터 변경 시 리스너라는 다른 개체에 알리는 기능을 데이터 개체에 제공할 수 있습니다. 식별 가능한 클래스에는 세 가지 유형, 즉 개체, 필드 및

developer.android.com

https://github.com/android/architecture-samples/tree/todo-mvvm-databinding

 

android/architecture-samples

A collection of samples to discuss and showcase different architectural tools and patterns for Android apps. - android/architecture-samples

github.com

https://medium.com/@bansooknam/android-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EB%B9%84%EA%B5%90-mvp-mvvm-svc-1-f24e5f338523

 

Android 아키텍처 비교–MVP, MVVM, SVC–1

1. Android에서 아키텍처 패턴은 왜 필요한가

medium.com

[리뷰]

https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/553

 

 

 


[6주차]

주제 : AAC ViewModel, LiveData

ViewModel vs AAC ViewModel  의 차이점이다. ACC VIewModel은 안드로이드를 위한 거고 일반적인 ViewModel과 비슷해보이지만 내부 동작방식이 다르다. 밑 사이트에 설명이 잘되어있다.

https://thdev.tech/androiddev/2018/08/05/Android-Architecture-Components-ViewModel-Inject/

 

Android Architecture Components ViewModel을 간단하게 초기화 하려면? |

I’m an Android Developer.

thdev.tech

[개념]

ViewModel 클래스는 수명 주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계되었습니다. ViewModel 클래스를 사용하면 화면 회전과 같이 구성을 변경할 때도 데이터를 유지할 수 있습니다.

 

 

[ViewModel 주의할점]

주의: ViewModel은 뷰, Lifecycle 또는 활동 컨텍스트 참조를 포함하는 클래스를 참조해서는 안 됩니다.

 

 

ViewModel의 수명 주기

ViewModel 객체의 범위는 ViewModel을 가져올 때 ViewModelProvider에 전달되는 Lifecycle로 지정됩니다. ViewModel은 범위가 지정된 Lifecycle이 영구적으로 경과될 때까지, 즉 활동에서는 활동이 끝날 때까지 그리고 프래그먼트에서는 프래그먼트가 분리될 때까지 메모리에 남아 있습니다.

그림 1에서는 활동이 회전을 거친 후 완료될 때까지 활동의 다양한 수명 주기 상태를 보여줍니다. 또한 관련 활동 수명 주기 옆에 ViewModel의 전체 기간도 보여줍니다. 이 특정 다이어그램에서는 활동의 상태를 보여줍니다. 동일한 기본 상태가 프래그먼트의 수명 주기에 적용됩니다.

 

 

 

[뷰모델 생성]

val model = ViewModelProviders.of(this)[MyViewModel::class.java]

한글 문서의 위 코드를 ktx extension 을 사용해서 다음과 같이 줄일 수 있다.

val model: MyViewModel by viewModels()

 

 

이를 위해 밑 gradle 추가 필요

compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.fragment:fragment-ktx:1.2.4'
    implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"

 

 

뷰모델을 팩토리 패턴으로 생성이 가능하다. 또한 같은 뷰모델을 여러 프래그먼트에서 공유할 수 있어서 데이터 공유 및 생명주기 등의 관리도 편하다.

private val viewModel: MovieSearchViewModel by viewModels {
        object : ViewModelProvider.Factory {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                return MovieSearchViewModel(myApplication.movieRepository) as T
            }
        }
    }

 

 

[Livedata 개념]

LiveData는 식별 가능한 데이터 홀더 클래스입니다. 식별 가능한 일반 클래스와 달리 LiveData는 수명 주기를 인식합니다. 즉 활동, 프래그먼트 또는 서비스와 같은 다른 앱 구성요소의 수명 주기를 고려합니다. 이러한 수명 주기 인식을 통해 LiveData는 활성 수명 주기 상태에 있는 앱 구성요소 관찰자만 업데이트합니다.

 

 

[LiveData(전자) 와 ObserveField(후자) 사용시 observe 차이점이다. LiveData 에는 액티비티의 컨텍스트가 매개변수로 들어간다는 차이점이 있다. 즉 LifecycleOwner 인스턴스을 첫번째 매개변수로 전달해줘야한다.  이렇게 하면 이 관찰자가 소유자와 연결된 Lifecycle 개체에 결합되어 있음을 나타내며 그 의미는 다음과 같다.

  • Lifecycle 개체가 활성 상태가 아니면 값이 변경되더라도 관찰자가 호출되지 않습니다.
  • Lifecycle 개체가 폐기되면 관찰자가 자동으로 삭제됩니다.
with(viewModel) { //LiveData 사용시
            toastMsg.observe(this@MovieSearchActivity, Observer {
                when (toastMsg.value) {
                    MovieSearchViewModel.MessageSet.LAST_PAGE -> showToast(getString(R.string.last_page_msg))
                    MovieSearchViewModel.MessageSet.EMPTY_QUERY -> showToast(getString(R.string.search_input_query_msg))
                    MovieSearchViewModel.MessageSet.NETWORK_ERROR -> showToast(getString(R.string.network_error_msg))
                    MovieSearchViewModel.MessageSet.SUCCESS -> showToast(getString(R.string.load_movie_success_msg))
                    MovieSearchViewModel.MessageSet.NO_RESULT -> showToast(getString(R.string.no_movie_error_msg))
                }
            })
 // ObserverField 사용시
 toastMsg.addOnPropertyChangedCallback(object :
                Observable.OnPropertyChangedCallback() {
                override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
                    when (toastMsg.get()) {
                        MovieSearchViewModel.MessageSet.LAST_PAGE -> showToast(getString(R.string.last_page_msg))
                        MovieSearchViewModel.MessageSet.EMPTY_QUERY -> showToast(getString(R.string.search_input_query_msg))
                        MovieSearchViewModel.MessageSet.NETWORK_ERROR -> showToast(getString(R.string.network_error_msg))
                        MovieSearchViewModel.MessageSet.SUCCESS -> showToast(getString(R.string.load_movie_success_msg))
                        MovieSearchViewModel.MessageSet.NO_RESULT -> showToast(getString(R.string.no_movie_error_msg))
                    }
                    toastMsg.set(MovieSearchViewModel.MessageSet.BASIC)
                }
            })

 

하지만 프래그먼트에서는 this 가 아닌 viewLifecycleOwner 를 매개변수로 줘야한다. 이유는 다음을 참고하면 나온다. (옵저버를 중복해서 하는 문제가 발생한다.)

Pluu Dev - Fragment Lifecycle과 LiveData - http://pluu.github.io/blog/android/2020/01/25/android-fragment-lifecycle/

 

Pluu Dev - Fragment Lifecycle과 LiveData

ActivityResult alpha02 vs alpha03 Posted on 02 Apr 2020 New ActivityResultRegistry Posted on 24 Mar 2020 SavedState 어떻게 저장되고 복원될까? Posted on 15 Mar 2020 SavedStateHandle을 다뤄봅니다 Posted on 20 Feb 2020

pluu.github.io

 

 

 

그리고 위와 같은 Observe 콜백 코드가 아닌 데이터바인딩으로 처리하고싶다면 데이터바인딩에도 LifecycleOwner 를 주입해줘야한다.

binding.lifecycleOwner = this

 

 

[참고할 것]

https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko

 

ViewModel 개요  |  Android 개발자  |  Android Developers

ViewModel을 사용하면 수명 주기를 인식하는 방식으로 UI 데이터를 관리할 수 있습니다.

developer.android.com

https://developer.android.com/topic/libraries/architecture/viewmodel

 

ViewModel 개요  |  Android 개발자  |  Android Developers

ViewModel을 사용하면 수명 주기를 인식하는 방식으로 UI 데이터를 관리할 수 있습니다.

developer.android.com

https://developer.android.com/topic/libraries/architecture/livedata?hl=ko

 

LiveData 개요  |  Android 개발자  |  Android Developers

LiveData를 사용하여 수명 주기를 인식하는 방식으로 데이터를 처리합니다.

developer.android.com

https://developer.android.com/topic/libraries/architecture/livedata

 

LiveData 개요  |  Android 개발자  |  Android Developers

LiveData를 사용하여 수명 주기를 인식하는 방식으로 데이터를 처리합니다.

developer.android.com

 

https://woovictory.github.io/2019/04/30/What-is-LiveData/

 

[안드로이드] LiveData

MVVM을 이해하기 위해 알아보는 LiveData 추후 더 자세한 내용을 정리할 예정. # LiveData란? 직역하면 살아있는 데이터? 이렇게 생각할 수 있다. LiveData는 LifeCycle을 알고 있는 DataType이라고 생각하면 좋다. 이처럼 LifeCycle을 알고 있으면 필요할 때 변경하고 필요하지 않을 때 변경하지 않을 수 있다. 또한,

woovictory.github.io

 

 

[리뷰]

https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/564

 

 


 

[7주차]

주제 : DI(Dependency Injection) -KOIN-

소프트웨어 엔지니어링에서 의존성 주입은 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉이다. "의존성"은 예를 들어 서비스로 사용할 수 있는 객체이다. 클라이언트가 어떤 서비스를 사용할 것인지 지정하는 대신, 클라이언트에게 무슨 서비스를 사용할 것인지를 말해주는 것이다.

 

코인은 안드로이드 이 의존성 주입을 쉽게 할 수 있도록 도와준다. (Dagger에 비해 러닝커브도 낮다고한다.)

대거와 코인의 차이점은 다음과 같다고한다.

 

[출처 : https://medium.com/harrythegreat/android-koin-%EB%A0%88%EB%B2%A8%EC%97%85-deep-drive-56b63b2e35d2]

Dagger

장점

  • 순수자바
  • 안정적이고 유연함
  • 런타임 에러가 발생하지 않음
  • 런타임시에 매우 빠름

단점

  • 컴파일시 오버헤드가 발생
  • 학습곡선이 상당함

Koin

장점

  • Annotation 과정이 없어 컴파일이빠름
  • 학습하기 쉽고 설치도 쉬움

단점

  • 런타임중 에러가 발생
  • Daager에 비해 런타임시 오버헤드가 있음

 

 

 

좋은 설명인 것 같아 이것도 가져왔다.

출처: https://jungwoon.github.io/android/2019/08/21/Koin/

 

 

 

자세한 개념은 밑의 [참고할 것] 의 사이트를 참고한다.

 

gradle 추가, 이 외에도 여러개 있는데 MVVM에서 다음은 기본

// koin
implementation 'org.koin:koin-androidx-viewmodel:2.1.5'

 

Application 예시 , manifest application name 에 추가해야함 

androidContext() 를 해줌으로 써 모듈에서 get으로 Context를 전달 받을 수 있음 androidContext() 로 안받아도 된다.

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

        }
    }
}

 

Repository 모듈 예시

val repositoryModule: Module = module {
    single<MovieRepository> { MovieRepositoryImpl(get(), get(), get()) }
    single<LoginRepository> { LoginRepositoryImpl(get()) }
}

 

 

ViewModel 모듈 예시

val viewModelModule: Module = module {
    viewModel { SplashViewModel(get()) }
    viewModel { LoginViewModel(get()) }
    viewModel { MovieSearchViewModel(get()) }
}

 

 

Acitivity 에서 ViewModel 모듈 가져다 쓰는거 예시 ( by viewModel() 사용)

import org.koin.androidx.viewmodel.ext.android.viewModel
class MovieSearchActivity : BaseActivity() {

    private lateinit var binding: ActivityMovieSearchBinding
    private lateinit var movieAdapter: MovieAdapter
    private val viewModel: MovieSearchViewModel by viewModel()

 

Retrofit2 모듈 예시

import com.mtjin.androidarchitecturestudy.api.ApiClient
import com.mtjin.androidarchitecturestudy.api.ApiInterface
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import org.koin.core.module.Module
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

val apiModule: Module = module {

    single<ApiInterface> { get<Retrofit>().create(ApiInterface::class.java) }

    single<Retrofit> {
        Retrofit.Builder()
            .baseUrl(ApiClient.BASE_URL)
            .client(get())
            .addConverterFactory(get<GsonConverterFactory>())
            .build()
    }

    single<GsonConverterFactory> { GsonConverterFactory.create() }

    single<OkHttpClient> {
        OkHttpClient.Builder()
            .run {
                addInterceptor(get<Interceptor>())
                build()
            }
    }

    single<Interceptor> {
        Interceptor { chain ->
            with(chain) {
                val newRequest = request().newBuilder()
                    .addHeader("X-Naver-Client-Id", "aaa")
                    .addHeader("X-Naver-Client-Secret", "aaa")
                    .build()
                proceed(newRequest)
            }
        }
    }
}

 

 

파라미터 전달 시 예제 (각각 액티비티, 모듈) ( parmetrsOf() 사용)

class MainActivity : AppCompatActivity() {
    //private val viewModel by viewModel<MainViewModel>{}
    private val viewModel by viewModel<MainViewModel>{
        //어떠한 파라미터를 넘기며 만들거냐
        val hashId = intent.getStringExtra("Key")
        parametersOf(hashId)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

 

val viewModelModule: Module = module {
    //iewModel { MainViewModel(get()) }
    viewModel { (hashId : String)-> MainViewModel(hashId, get())}
}

 

위의 방법을 사용해서 만약 해당 레포지토리가 두개가 있는 경우 구분해주는 방법 (모듈)

val repositoryModule: Module = module {
    single<MainRepository> {MainRepositoryImpl()}
    /*메인레포를 2개가 있는 경우 구분해주는 법,
    * viewModel { (hashId : String)-> MainViewModel(hashId, get("prod))} 식으로 해주면됨
    * */
    //single<MainRepository>(named("prod")) {MainRepositoryImpl()}
    //single<MainRepository>(named("test")) {MockRepositoryImpl()}
}

 

+)

val apiModule: Module = module {
    single<ApiInterface>(named("tour")) { get<Retrofit>().create(ApiInterface::class.java) }
   single<LocalPageRepository> { LocalPageRepositoryImpl(get(named("tour"))) }

 

 

[참고할 것]

https://gmlwjd9405.github.io/2018/11/09/dependency-injection.html

 

[Design Pattern] DI란 (Dependency Injection) - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

https://jungwoon.github.io/android/2019/08/21/Koin/

 

Koin 정리하기 - Jungwoon Blog

Koin 정리하기 이번에는 DI 라이브러리 중 하나인 Koin에 대해서 정리를 해보고자 합니다. 기존에 DI 라이브러리로 유명한건 Dagger 인데, Dagger가 학습 곡선이 높아서 우선 상대적으로 학습 곡선이 낮은 Koin을 학습하고 그 이후에 Dagger도 같이 볼까 합니다. DI(Dependency Injection)이란? DI 라이브러리인 Koin을 설명하기 전에 우선 DI(Dependency Injection)가 무엇인지부터 설명을 하려고 합니

jungwoon.github.io

https://speakerdeck.com/jakewharton/dependency-injection-with-dagger-2-devoxx-2014

 

Dependency Injection with Dagger 2 (Devoxx 2014)

Dagger is a fast dependency injector for both Java and Android. Its second major version not only brings new features for parity with other frameworks but continues to push the boundaries of speed. The concepts covered in this talk apply to both Java targe

speakerdeck.com

https://developer.android.com/training/dependency-injection

 

Dependency injection in Android  |  Android 개발자  |  Android Developers

Dependency injection (DI) is a technique widely used in programming and well suited to Android development. By following the principles of DI, you lay the groundwork for good app architecture. Implementing dependency injection provides you with the followi

developer.android.com

https://insert-koin.io/

 

 

 

[리뷰]

https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/580

 

 

 

 

 

 

 


 

 

[Multi Module]

KOIN , MVVM 등 아키텍처 패턴으로 모듈화 및 구조를 분리 했지만 모듈화를 사용하여 더 분리하여 결합도를 낮추고 응집도를 더 높일 수 있다.

data(Repository) , local, remote 로 모듈화 하는 작업을 을했고 각 모듈이 model(data class) 를 전달할 때 변환해주는 mapper 를 구현했다. app 에서는 data 모듈의 model을 사용하며 local, remote 에서 mapper 를 만들어 local, remote 의 model(data class)를 data로 mapper 로 각 알맞게 변환하여 전달하게 했다.

그래서 전체적인 구조를 보면 app은 data, local, remote를 가지게 되고 local, remote는 data 모듈을 가지게 된다.

그리고 각 모듈은 기본적으로 model, di(module), source 패키지를 가지고 있게하였다. 

P.S 더 쪼개면 feature 까지 쪼갠다한다. 

 

모듈을 만들면 settings.gradle 에 다음과 같이 모듈들이 생긴다. 만약 모듈아이콘이 안뜨고 합쳐지면 rootProject.name을 삭제해주면 된다.

 

 

이 모듈들을 불러오기 위해 자신의 gradle 에 다음과 같이 추가해주면 된다. (app 수준의 gradle)

dependencies {
    //multi module
    implementation project(':data')
    implementation project(':remote')
    implementation project(':local')

}

 

app, data(Repo), local, remote 모듈로 구현을 했는데

app 이 data, local, remote 모듈을 물고 있고

local, remote 모듈이 data 모듈을 물고 있다. (local, remote module 은 data 만 위 처럼 디펜던시 추가해주면 된다.)

그리고 data 모듈은 app 과 local, remote 의 model(data class)를 변환해주기 위한 mapper 를 구현한다.

ex)

local

package com.mtjin.local.mapper

import com.mtjin.local.model.search.Movie
import com.mtjin.data.model.search.Movie as dataMovie

fun mapperMovieListLocalToData(movies: List<Movie>): List<dataMovie> {
    return movies.toList().map {
        dataMovie(
            it.actor,
            it.director,
            it.image,
            it.link,
            it.pubDate,
            it.subtitle,
            it.title,
            it.userRating
        )
    }
}

fun mapperMovieListDataToLocal(movies: List<dataMovie>): List<Movie> {
    return movies.toList().map {
        Movie(
            it.actor,
            it.director,
            it.image,
            it.link,
            it.pubDate,
            it.subtitle,
            it.title,
            it.userRating
        )
    }
}

remote

package com.mtjin.remote.mapper

import com.mtjin.remote.model.search.Movie
import com.mtjin.data.model.search.Movie as dataMovie

fun mapperMovieListRemoteToData(movies: List<Movie>): List<dataMovie> {
    return movies.toList().map {
        dataMovie(
            it.actor,
            it.director,
            it.image,
            it.link,
            it.pubDate,
            it.subtitle,
            it.title,
            it.userRating
        )
    }
}

 

 

 

그리고 각 모듈에서 Impl 클래서에선느 외부에서 접근못하게 internal 접근자를 붙여준다. 추가로 data 모듈 에서는 LocalDataSource 와 RemoteDataSource 인터페이스를 갖고있고 remote, local 모듈에서는 이 인터페이스를 Impl 클래스로 구현해놓아야한다. 이를 통해 data의 Repository 에서 localDataSource, RemoteDataSource를 사용할 수 있게 해주고 실제 구현은 local, remote 모듈에서 해주는 형식이 된다.

 

그리고 app 말고 다른 모듈들에서는 필요없는 안드로이드 의존성을 제거해주도록 한다.

data 모듈 예시)

dependencies {
    /*  제거
	implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
   */
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    // koin
    implementation 'org.koin:koin-core:2.1.5'
}

local 예시)

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    // koin
    implementation 'org.koin:koin-androidx-viewmodel:2.1.5'
    // Room
    implementation "androidx.room:room-runtime:2.2.5"
    testImplementation "androidx.room:room-testing:2.2.5"
    kapt "androidx.room:room-compiler:2.2.5"
    implementation 'androidx.room:room-ktx:2.2.5'
    //multi module
    implementation project(':data')
}

remote 예시)

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    //retrofit2
    implementation 'com.squareup.retrofit2:retrofit:2.7.2'
    implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
    // koin
    implementation 'org.koin:koin-androidx-viewmodel:2.1.5'
    //multi module
    implementation project(':data')
}

 

[리뷰]

https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/593

 

 

 


[RxJava2 적용]

 

다음은 RxJava2 공부 후 적용해본 프로젝트입니다. (네트워크 로직에만 적용했습니다.)

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

 

이에 대한 코드 설명은 다음 링크에서 다뤄봤습니다.

youngest-programming.tistory.com/288?category=925232

 

[RxJava] MVVM - Model Layer 네트워크 통신에 적용

https://github.com/mtjin/android-architecture-study-movieapp/tree/master/BACK_UP/9-RxJava/AndroidArchitectureStudy mtjin/android-architecture-study-movieapp 안드로이드 아키텍처 스터디 정리. Contribu..

youngest-programming.tistory.com

 


[Koin -> Dagger2 적용]

 

 

Dagger2 적용한 프로젝트입니다.

youngest-programming.tistory.com/351

 

[코틀린] DI 대거(Dagger2) 공부자료 및 적용해보기 (Koin -> Dagger)

안녕하세요 ㅎㅎ 요즘 블로그 포스팅할 시간이 없어 못하고있네요 ㅠㅠ 저도 현재 사용해야할 기술 중 하나이고 처음 공부 중이기 떄문에 DI 와 Dagger2 에 대한 개념은 추후에 더 학습 후 정리하��

youngest-programming.tistory.com

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

 

mtjin/android-architecture-study-movieapp

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

github.com

 


[클린아키텍처 학습 및 적용]

https://youngest-programming.tistory.com/484

 

[안드로이드] 클린 아키텍처(Clean Architecture) 정리 및 구현

[2021-04-28 업데이트] [프로젝트] github.com/mtjin/mtjin-android-clean-architecture-movieapp mtjin/mtjin-android-clean-architecture-movieapp Clean Architecture 학습 및 구현. Contribute to mtjin/mtjin..

youngest-programming.tistory.com

https://github.com/mtjin/mtjin-android-clean-architecture-movieapp

 

GitHub - mtjin/mtjin-android-clean-architecture-movieapp: Clean Architecture 학습 및 구현

Clean Architecture 학습 및 구현. Contribute to mtjin/mtjin-android-clean-architecture-movieapp development by creating an account on GitHub.

github.com

 

 

THE END....

 

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

 

 

 

 

 

728x90
Comments