Android MVVM ViewModel에서 컨텍스트를 얻는 방법


90

내 Android 앱에서 MVVM 패턴을 구현하려고합니다. ViewModels에 안드로이드 특정 코드가 포함되어서는 안된다는 것을 읽었지만 (테스트를 더 쉽게하기 위해), 여러 가지 (xml에서 리소스 가져 오기, 기본 설정 초기화 등)에 컨텍스트를 사용해야합니다. 이를 수행하는 가장 좋은 방법은 무엇입니까? I 톱 AndroidViewModel내가하지 않도록 그 뷰 모델에 있어야하는 경우 해요 있도록 애플리케이션 컨텍스트에 대한 참조를 가지고, 그러나 그것은 안드로이드 특정 코드가 포함되어 있습니다. 또한 그것들은 활동 라이프 사이클 이벤트와 연결되지만, 구성 요소의 범위를 관리하기 위해 단검을 사용하고 있으므로 그것이 어떻게 영향을 미칠지 잘 모르겠습니다. 저는 MVVM 패턴과 Dagger를 처음 사용하므로 도움을 주시면 감사하겠습니다!


누군가가 사용하려고 AndroidViewModel하지만 얻을 경우에 대비 Cannot create instance exception하여이 답변을 참조 할 수 있습니다. stackoverflow.com/a/62626408/1055241
gprathour

ViewModel에서 Context를 사용해서는 안되며, 대신 UseCase를 만들어 컨텍스트를 가져옵니다.
Ruben Caster

답변:


71

에서 Application제공 하는 컨텍스트를 사용할 수 있으며 단순히 참조 를 포함하는 AndroidViewModel확장해야합니다 .AndroidViewModelViewModelApplication


매력처럼 일했다!
SPM

누군가 이것을 코드로 보여줄 수 있습니까? 저는 자바에 있습니다
Biswas Khayargoli

55

Android 아키텍처 구성 요소보기 모델의 경우

메모리 누수로 인해 Activity Context를 Activity의 ViewModel에 전달하는 것은 좋은 습관이 아닙니다.

따라서 ViewModel에서 컨텍스트를 얻으려면 ViewModel 클래스가 Android View Model 클래스를 확장해야합니다 . 이렇게하면 아래 예제 코드와 같이 컨텍스트를 얻을 수 있습니다.

class ActivityViewModel(application: Application) : AndroidViewModel(application) {

    private val context = getApplication<Application>().applicationContext

    //... ViewModel methods 

}

2
애플리케이션 매개 변수와 일반 ViewModel을 직접 사용하지 않는 이유는 무엇입니까? "getApplication <Application> ()"에는 아무 의미가 없습니다. 단지 상용구를 추가합니다.
놀라운 1

50

ViewModels에 테스트를 더 쉽게 만드는 Android 특정 코드가 포함되어서는 안됩니다.

ViewModels에 Context 인스턴스 또는 Context를 유지하는 뷰 또는 기타 개체와 같은 항목이 포함되어서는 안되는 이유는 활동 및 조각과는 별도의 수명주기가 있기 때문입니다.

이것이 의미하는 바는 앱에서 회전 변경을한다고 가정 해 봅시다. 이로 인해 활동과 조각이 스스로 파괴되어 스스로를 다시 만듭니다. ViewModel은이 상태 동안 지속되도록되어 있으므로 파괴 된 Activity에 대한 View 또는 Context를 여전히 보유하고있는 경우 충돌 및 기타 예외가 발생할 가능성이 있습니다.

원하는 작업을 수행하는 방법에 관해서는 MVVM 및 ViewModel이 JetPack의 데이터 바인딩 구성 요소와 잘 작동합니다. 일반적으로 String, int 등을 저장하는 대부분의 경우 Databinding을 사용하여 View가 직접 표시하도록 할 수 있으므로 ViewModel 내에 값을 저장할 필요가 없습니다.

그러나 데이터 바인딩을 원하지 않는 경우 생성자 또는 메서드 내부에 Context를 전달하여 리소스에 액세스 할 수 있습니다. ViewModel 내부에 해당 컨텍스트의 인스턴스를 보유하지 마십시오.


1
Android 특정 코드를 포함하려면 일반 JUnit 테스트보다 훨씬 느린 계측 테스트를 실행해야한다는 것을 이해했습니다. 현재 클릭 메서드에 데이터 바인딩을 사용하고 있지만 xml 또는 기본 설정에서 리소스를 가져 오는 데 어떻게 도움이 될지 모르겠습니다. 선호도를 위해 모델 내부에도 컨텍스트가 필요하다는 것을 깨달았습니다. 내가 현재하고있는 것은 Dagger가 응용 프로그램 컨텍스트를 주입하도록하는 것입니다 (컨텍스트 모듈은 응용 프로그램 클래스 내의 정적 메서드
Vincent Williams

@VincentWilliams 예, ViewModel을 사용하면 UI 구성 요소에서 코드를 추상화하여 테스트를 더 쉽게 수행 할 수 있습니다. 그러나 내가 말하고있는 것은 컨텍스트, 뷰 등을 포함하지 않는 주된 이유는 테스트 이유 때문이 아니라 충돌 및 기타 오류를 피할 수있는 ViewModel의 수명주기 때문이라는 것입니다. 데이터 바인딩의 경우 코드의 리소스에 액세스하는 데 필요한 대부분의 시간이 데이터 바인딩이 직접 수행 할 수있는 레이아웃에 해당 문자열, 색상, 치수를 적용해야하기 때문에 리소스에 도움이 될 수 있습니다.
Jackey

오 확인 말인지하지만이 된 SharedPreferences를 초기화 또한 모델에서 사용하기 위해 액세스 문자열이 필요 (이 대신 내가 생각 XML의 상수 클래스에 넣을 수)와 이후 데이터 바인딩이 경우 나 도움이되지 않습니다 것을 볼
빈센트 윌리엄스

3
값 형식 viewmodel을 기반으로 텍스트보기에서 텍스트를 전환하려면 문자열을 현지화해야하므로 컨텍스트없이 내 viewmodel에서 리소스를 가져와야합니다. 리소스에 어떻게 액세스 할 수 있습니까?
Srishti Roy

3
@SrishtiRoy 데이터 바인딩을 사용하는 경우 뷰 모델의 값을 기반으로 TextView의 텍스트를 쉽게 전환 할 수 있습니다. 이 모든 것이 레이아웃 파일 내에서 발생하기 때문에 ViewModel 내부의 컨텍스트에 액세스 할 필요가 없습니다. 그러나 ViewModel 내에서 Context를 사용해야하는 경우 ViewModel 대신 AndroidViewModel을 사용하는 것이 좋습니다. AndroidViewModel에는 getApplication ()으로 호출 할 수있는 애플리케이션 컨텍스트가 포함되어 있으므로 ViewModel에 컨텍스트가 필요한 경우 컨텍스트 요구 사항을 충족해야합니다.
Jackey

15

짧은 대답-하지 마십시오

왜 ?

뷰 모델의 전체 목적을 무너 뜨립니다.

뷰 모델에서 수행 할 수있는 거의 모든 작업은 LiveData 인스턴스 및 기타 다양한 권장 접근 방식을 사용하여 활동 / 단편에서 수행 할 수 있습니다.


21
왜 AndroidViewModel 클래스가 존재합니까?
Alex Berdnikov

1
@AlexBerdnikov MVVM의 목적은 MVP보다 ViewModel에서 뷰 (Activity / Fragment)를 분리하는 것입니다. 테스트하기 더 쉬울 것입니다.
hushed_voice

3
@free_style 설명해 주셔서 감사합니다.하지만 질문은 여전히 ​​남아 있습니다. ViewModel에서 컨텍스트를 유지해서는 안되는 경우 AndroidViewModel 클래스가 존재하는 이유는 무엇입니까? 전체 목적은 애플리케이션 컨텍스트를 제공하는 것입니다.
Alex Berdnikov

6
@AlexBerdnikov viewmodel 내부에서 활동 컨텍스트를 사용하면 메모리 누수가 발생할 수 있습니다. 따라서 AndroidViewModel 클래스를 사용하면 메모리 누수를 일으키지 않는 Application Context가 제공됩니다. 따라서 AndroidViewModel을 사용하는 것이 활동 컨텍스트를 전달하는 것보다 낫습니다. 하지만 여전히 그렇게하면 테스트가 어려워집니다. 이것이 내 생각입니다.
hushed_voice 19-09-25

1
저장소에서 res / raw 폴더의 파일에 액세스 할 수 없습니까?
Fugogugo

14

ViewModel에서 직접 Context를 사용하는 대신 수행 한 작업은 필요한 리소스를 제공하는 ResourceProvider와 같은 공급자 클래스를 만들고 ViewModel에 해당 공급자 클래스를 삽입했습니다.


1
AppModule에서 Dagger와 함께 ResourcesProvider를 사용하고 있습니다. ResourcesProvider 또는 AndroidViewModel에서 컨텍스트를 얻는 좋은 접근 방식이 리소스에 대한 컨텍스트를 얻는 것이 더 낫습니까?
Usman Rana

@Vincent : ResourceProvider를 사용하여 ViewModel 내에서 Drawable을 얻는 방법은 무엇입니까?
HoangVu

당신은 같은 방법을 추가 할 수 @Vegeta getDrawableRes(@DrawableRes int id)ResourceProvider 클래스 내부
빈센트 윌리엄스에게

1
이것은 프레임 워크 종속성이 도메인 논리 (ViewModels)로 경계를 넘지 않아야한다는 Clean Architecture 접근 방식에 위배됩니다.
IgorGanapolsky

1
@IgorGanapolsky VM은 정확히 도메인 로직이 아닙니다. 도메인 로직은 인터랙 터 및 리포지토리와 같은 다른 클래스입니다. VM은 도메인과 상호 작용하기 때문에 "접착제"범주에 속하지만 직접적으로는 아닙니다. VM이 도메인의 일부인 경우 너무 많은 책임을 부여하고 있으므로 패턴을 사용하는 방법을 재고해야합니다.
mradzinski

8

요약 : ViewModels에서 Dagger를 통해 애플리케이션의 컨텍스트를 주입하고이를 사용하여 리소스를로드합니다. 이미지를로드해야하는 경우 Databinding 메서드의 인수를 통해 View 인스턴스를 전달하고 해당 View 컨텍스트를 사용합니다.

MVVM은 좋은 아키텍처이며 확실히 Android 개발의 미래이지만 여전히 녹색 인 몇 가지 사항이 있습니다. 예를 들어 MVVM 아키텍처의 계층 통신을 살펴보면 여러 개발자 (아주 잘 알려진 개발자)가 LiveData를 사용하여 서로 다른 방식으로 서로 다른 계층을 통신하는 것을 보았습니다. 그들 중 일부는 LiveData를 사용하여 ViewModel을 UI와 통신하지만 콜백 인터페이스를 사용하여 Repositories와 통신하거나 Interactors / UseCases가 있고 LiveData를 사용하여 통신합니다. 여기서 요점은 모든 것이 아직 100 % 정의 된 것은 아니라는 것입니다 .

즉, 특정 문제에 대한 내 접근 방식은 DI를 통해 응용 프로그램의 컨텍스트를 사용하여 내 ViewModel에서 사용하여 strings.xml에서 String과 같은 것을 가져 오는 것입니다.

이미지로드를 처리하는 경우 데이터 바인딩 어댑터 메서드에서 View 개체를 전달하고 View의 컨텍스트를 사용하여 이미지를로드하려고합니다. 왜? 일부 기술 (예 : Glide)은 애플리케이션의 컨텍스트를 사용하여 이미지를로드하는 경우 문제가 발생할 수 있기 때문입니다.

도움이 되었기를 바랍니다.


5
TL; DR은 상단에 표시한다
자크 Koorts

1
답변 주셔서 감사합니다. 그러나 viewmodel을 androidviewmodel에서 확장하고 클래스 자체가 제공하는 내장 컨텍스트를 사용할 수 있다면 왜 dagger를 사용하여 컨텍스트를 삽입합니까? 특히 Dagger와 MVVM이 함께 작동하도록 만드는 엄청난 양의 상용구 코드를 고려하면 다른 솔루션이 훨씬 더 명확 해 보입니다. 이것에 대한 당신의 생각은 무엇입니까?
Josip Domazet

7

다른 사람들이 언급했듯이 AndroidViewModel앱을 얻기 위해 파생 할 수있는 Context것이 있지만 주석에서 수집 한 내용에서 MVVM 목적을 무력화하는 @drawables 를 조작하려고합니다 ViewModel.

일반적으로를 가지고 있어야한다는 Context것은 ViewModel거의 보편적으로 Views와 ViewModels.

ViewModel드로어 블 을 해결하고이를 Activity / Fragment에 공급하는 대신 Fragment / Activity가 .NET Core가 소유 한 데이터를 기반으로 드로어 블을 저글링하도록하는 것이 ViewModel좋습니다. on / off 상태에 대한 뷰에 다른 드로어 블을 표시해야한다고 가정 해 보겠습니다. ViewModel이는 (아마도 부울) 상태를 유지해야하지만 View그에 따라 드로어 블을 선택하는 것이 몫입니다.

DataBinding으로 매우 쉽게 수행 할 수 있습니다 .

<ImageView
...
app:src="@{viewModel.isOn ? @drawable/switch_on : @drawable/switch_off}"
/>

상태와 드로어 블이 더 많은 경우 레이아웃 파일의 다루기 힘든 논리를 피하기 위해 값을 (예 : 카드 슈트) 로 변환 하는 사용자 정의 BindingAdapter 를 작성할 수 있습니다.EnumR.drawable.*

또는 Context내부에서 사용하는 일부 구성 요소가 필요할 수 있습니다. ViewModel그런 다음 외부에서 구성 요소를 만들고 ViewModel전달합니다. DI 또는 싱글 톤을 사용하거나 in /을 Context초기화하기 직전에 종속 구성 요소를 만들 수 있습니다 .ViewModelFragmentActivity

귀찮은 이유 : Context는 Android 전용이며 ViewModels 에있는 항목에 따라 달라지는 것은 좋지 않습니다. 단위 테스트를 방해합니다. 반면에 자신의 구성 요소 / 서비스 인터페이스는 완전히 제어 할 수 있으므로 테스트를 위해 쉽게 모의 할 수 있습니다.


5

애플리케이션 컨텍스트에 대한 참조가 있지만 Android 특정 코드를 포함합니다.

좋은 소식 Mockito.mock(Context.class)입니다. 테스트에서 원하는대로 컨텍스트를 사용 하고 반환하도록 만들 수 있습니다 .

따라서 ViewModel평소처럼 a 를 사용하고 평소처럼 ViewModelProviders.Factory를 통해 ApplicationContext를 제공하십시오.


3

getApplication().getApplicationContext()ViewModel 내 에서 애플리케이션 컨텍스트에 액세스 할 수 있습니다 . 이것은 리소스, 기본 설정 등에 액세스하는 데 필요한 것입니다.


내 질문의 범위를 좁힐 것 같습니다. viewmodel 내부에 컨텍스트 참조를 갖는 것이 나쁘고 (테스트에 영향을주지 않습니까?) AndroidViewModel 클래스를 사용하면 어떤 식 으로든 Dagger에 영향을 미칩니 까? 활동 라이프 사이클과 연결되어 있지 않습니까? 나는 구성 요소의 라이프 사이클을 제어하기 위해 단검을 사용하고 있습니다
빈센트 윌리엄스

14
ViewModel클래스는없는 getApplication방법을.
beroal

4
아니,하지만 AndroidViewModel않습니다
4Oh4

1
하지만 생성자의 응용 프로그램 인스턴스를 통과해야, 그것에서 응용 프로그램 인스턴스를 액세스와 같은 단지 동일합니다
존 사르를

2
애플리케이션 컨텍스트를 갖는 것은 큰 문제가되지 않습니다. 조각 / 활동이 파괴되고 뷰 모델에 현재 존재하지 않는 컨텍스트에 대한 참조가 여전히 있으면 지루하기 때문에 활동 / 조각 컨텍스트를 갖고 싶지 않습니다. 그러나 APPLICATION 컨텍스트는 절대로 파괴되지 않지만 VM에는 여전히 참조가 있습니다. 권리? 앱이 종료되지만 Viewmodel은 종료되지 않는 시나리오를 상상할 수 있습니까? :)
user1713450

3

ViewModel을 사용하는 동기는 Java 코드와 Android 코드를 분리하는 것이기 때문에 ViewModel에서 Android 관련 개체를 사용하면 안됩니다. 따라서 비즈니스 로직을 개별적으로 테스트 할 수 있고 Android 구성 요소와 비즈니스 로직의 별도 레이어를 갖게됩니다. 충돌이 발생할 수 있으므로 ViewModel에 컨텍스트가 없어야합니다.


2
이는 공정한 관찰이지만 일부 백엔드 라이브러리에는 여전히 MediaStore와 같은 애플리케이션 컨텍스트가 필요합니다. 아래 4gus71n의 답변은 타협하는 방법을 설명합니다.
Bryan W. Wagner

1
예, 애플리케이션 컨텍스트를 사용할 수 있지만 활동 컨텍스트는 사용할 수 없습니다. 애플리케이션 컨텍스트는 애플리케이션 라이프 사이클 전체에 걸쳐 있지만 활동 컨텍스트를 비동기 프로세스에 전달하면 메모리 누수가 발생할 수 있으므로 활동 컨텍스트가 아닙니다. Context.하지만 애플리케이션 컨텍스트 인 경우에도 비동기 프로세스에 컨텍스트를 전달하지 않도록주의해야합니다.
Rohit Sharma 19

2

수업을 SharedPreferences사용할 때 어려움이 ViewModel있었기 때문에 위의 답변에서 조언을 받아을 사용하여 다음을 수행했습니다 AndroidViewModel. 이제 모든 것이 멋져 보입니다.

에 대한 AndroidViewModel

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;

import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.preference.PreferenceManager;

public class HomeViewModel extends AndroidViewModel {

    private MutableLiveData<String> some_string;

    public HomeViewModel(Application application) {
        super(application);
        some_string = new MutableLiveData<>();
        Context context = getApplication().getApplicationContext();
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        some_string.setValue("<your value here>"));
    }

}

그리고 Fragment

import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;


public class HomeFragment extends Fragment {


    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {
        final View root = inflater.inflate(R.layout.fragment_home, container, false);
        HomeViewModel homeViewModel = ViewModelProviders.of(this).get(HomeViewModel.class);
        homeViewModel.getAddress().observe(getViewLifecycleOwner(), new Observer<String>() {
            @Override
            public void onChanged(@Nullable String address) {


            }
        });
        return root;
    }
}

0

나는 이것을 이렇게 만들었습니다.

@Module
public class ContextModule {

    @Singleton
    @Provides
    @Named("AppContext")
    public Context provideContext(Application application) {
        return application.getApplicationContext();
    }
}

그런 다음 AppComponent에 ContextModule.class를 추가했습니다.

@Component(
       modules = {
                ...
               ContextModule.class
       }
)
public interface AppComponent extends AndroidInjector<BaseApplication> {
.....
}

그런 다음 ViewModel에 컨텍스트를 삽입했습니다.

@Inject
@Named("AppContext")
Context context;

0

다음 패턴을 사용하십시오.

class NameViewModel(
val variable:Class,application: Application):AndroidViewModel(application){
   body...
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.