Android ViewModel 추가 인수


107

AndroidViewModel응용 프로그램 컨텍스트를 제외하고 내 사용자 지정 생성자에 추가 인수를 전달하는 방법이 있습니까? 예:

public class MyViewModel extends AndroidViewModel {
    private final LiveData<List<MyObject>> myObjectList;
    private AppDatabase appDatabase;

    public MyViewModel(Application application, String param) {
        super(application);
        appDatabase = AppDatabase.getDatabase(this.getApplication());

        myObjectList = appDatabase.myOjectModel().getMyObjectByParam(param);
    }
}

그리고 내 커스텀 ViewModel클래스 를 사용하고 싶을 때이 코드를 내 조각에 사용합니다.

MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)

그래서 추가 인수 String param를 내 custom 에 전달하는 방법을 모르겠습니다 ViewModel. 애플리케이션 컨텍스트 만 전달할 수 있지만 추가 인수는 전달할 수 없습니다. 도움을 주시면 감사하겠습니다. 감사합니다.

편집 : 일부 코드를 추가했습니다. 이제 더 나아 졌으면합니다.


세부 사항 및 코드 추가
hugo

오류 메시지는 무엇입니까?
Moses Aprico

오류 메시지가 없습니다. ViewModelProvider가 AndroidViewModel 개체를 만드는 데 사용되므로 생성자에 대한 인수를 어디에서 설정해야할지 모르겠습니다.
Mario Rudman

답변:


213

ViewModel에 대한 팩토리 클래스가 있어야합니다.

public class MyViewModelFactory implements ViewModelProvider.Factory {
    private Application mApplication;
    private String mParam;


    public MyViewModelFactory(Application application, String param) {
        mApplication = application;
        mParam = param;
    }


    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        return (T) new MyViewModel(mApplication, mParam);
    }
}

뷰 모델을 인스턴스화 할 때 다음과 같이합니다.

MyViewModel myViewModel = ViewModelProvider(this, new MyViewModelFactory(this.getApplication(), "my awesome param")).get(MyViewModel.class);

kotlin의 경우 위임 된 속성을 사용할 수 있습니다.

val viewModel: MyViewModel by viewModels { MyViewModelFactory(getApplication(), "my awesome param") }

또 다른 새로운 옵션 이 있습니다. 팩토리의 인스턴스화 를 구현 HasDefaultViewModelProviderFactory하고 재정의 getDefaultViewModelProviderFactory()한 다음 팩토리를 호출 ViewModelProvider(this)하거나 호출 by viewModels()하지 않는 것 입니다.


4
모든 ViewModel클래스에 ViewModelFactory가 필요 합니까 ?
dmlebron

6
그러나 모든 사람 ViewModel은 다른 DI를 가질 수 있습니다. 어떤 인스턴스가 create()메서드에서 반환되는지 어떻게 알 수 있습니까?
dmlebron

1
방향을 변경하면 ViewModel이 다시 생성됩니다. 매번 공장을 만들 수 없습니다.
Tim

3
그건 사실이 아니야. 새로운 ViewModel창조는 방법을 막습니다 get(). 문서 기반 : "이 ViewModelProvider와 관련된 기존 ViewModel을 반환하거나 범위 (일반적으로 조각 또는 활동)에 새 ViewModel을 만듭니다." 참조 : developer.android.com/reference/android/arch/lifecycle/…
mlyko

2
방법 사용에 대한 return modelClass.cast(new MyViewModel(mApplication, mParam))경고를 제거하는
jackycflau

23

종속성 주입으로 구현

이것은 프로덕션 코드에 대해 더 발전되고 더 좋습니다.

Square의 AssistedInject 인 Dagger2 는 네트워크 및 데이터베이스 요청을 처리하는 리포지토리와 같은 필수 구성 요소를 삽입 할 수있는 ViewModels에 대한 프로덕션 준비 구현을 제공합니다. 또한 활동 / 단편에 인수 / 매개 변수를 수동으로 삽입 할 수 있습니다. 다음 은 Gabor Varadi의 자세한 게시물 인 Dagger Tips를 기반으로 코드 요점으로 구현 하는 단계에 대한 간결한 개요입니다 .

Dagger Hilt는 20 년 7 월 12 일 알파 버전의 차세대 솔루션으로, 라이브러리가 릴리스 상태에 있으면 더 간단한 설정으로 동일한 사용 사례를 제공합니다.

Kotlin에서 Lifecycle 2.2.0 으로 구현

인수 / 매개 변수 전달

// Override ViewModelProvider.NewInstanceFactory to create the ViewModel (VM).
class SomeViewModelFactory(private val someString: String): ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T = SomeViewModel(someString) as T
} 

class SomeViewModel(private val someString: String) : ViewModel() {
    init {
        //TODO: Use 'someString' to init process when VM is created. i.e. Get data request.
    }
}

class Fragment: Fragment() {
    // Create VM in activity/fragment with VM factory.
    val someViewModel: SomeViewModel by viewModels { SomeViewModelFactory("someString") } 
}

인수 / 매개 변수로 SavedState 활성화

class SomeViewModelFactory(
        private val owner: SavedStateRegistryOwner,
        private val someString: String) : AbstractSavedStateViewModelFactory(owner, null) {
    override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, state: SavedStateHandle) =
            SomeViewModel(state, someString) as T
}

class SomeViewModel(private val state: SavedStateHandle, private val someString: String) : ViewModel() {
    val feedPosition = state.get<Int>(FEED_POSITION_KEY).let { position ->
        if (position == null) 0 else position
    }
        
    init {
        //TODO: Use 'someString' to init process when VM is created. i.e. Get data request.
    }
        
     fun saveFeedPosition(position: Int) {
        state.set(FEED_POSITION_KEY, position)
    }
}

class Fragment: Fragment() {
    // Create VM in activity/fragment with VM factory.
    val someViewModel: SomeViewModel by viewModels { SomeViewModelFactory(this, "someString") } 
    private var feedPosition: Int = 0
     
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        someViewModel.saveFeedPosition((contentRecyclerView.layoutManager as LinearLayoutManager)
                .findFirstVisibleItemPosition())
    }    
        
    override fun onViewStateRestored(savedInstanceState: Bundle?) {
        super.onViewStateRestored(savedInstanceState)
        feedPosition = someViewModel.feedPosition
    }
}

공장에서 만든 오버라이드 (override) 동안 나는 체크되지 않은 캐스트 'T에 ItemViewModel'를 말하는 경고를 얻을
Ssenyonjo

1
그 경고는 지금까지 나에게 문제가되지 않았습니다. 그러나 ViewModel 팩토리를 리팩터링하여 조각을 통해 인스턴스를 생성하는 대신 Dagger를 사용하여 주입 할 때 자세히 살펴 보겠습니다.
Adam Hurwitz

15

여러 다른 뷰 모델간에 공유되는 한 공장의 경우 다음과 같이 mlyko의 답변을 확장합니다.

public class MyViewModelFactory extends ViewModelProvider.NewInstanceFactory {
    private Application mApplication;
    private Object[] mParams;

    public MyViewModelFactory(Application application, Object... params) {
        mApplication = application;
        mParams = params;
    }

    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        if (modelClass == ViewModel1.class) {
            return (T) new ViewModel1(mApplication, (String) mParams[0]);
        } else if (modelClass == ViewModel2.class) {
            return (T) new ViewModel2(mApplication, (Integer) mParams[0]);
        } else if (modelClass == ViewModel3.class) {
            return (T) new ViewModel3(mApplication, (Integer) mParams[0], (String) mParams[1]);
        } else {
            return super.create(modelClass);
        }
    }
}

뷰 모델 인스턴스화 :

ViewModel1 vm1 = ViewModelProviders.of(this, new MyViewModelFactory(getApplication(), "something")).get(ViewModel1.class);
ViewModel2 vm2 = ViewModelProviders.of(this, new MyViewModelFactory(getApplication(), 123)).get(ViewModel2.class);
ViewModel3 vm3 = ViewModelProviders.of(this, new MyViewModelFactory(getApplication(), 123, "something")).get(ViewModel3.class);

다른 생성자를 가진 다른 뷰 모델.


8
다음과 같은 두 가지 이유 때문에이 방법을 권장하지 않습니다. 1) 공장의 매개 변수가 형식에 안전하지 않습니다. 이렇게하면 런타임에 코드를 손상시킬 수 있습니다. 가능하면 항상이 접근 방식을 피하십시오. 2) 뷰 모델 유형을 확인하는 것은 실제로 OOP 방식이 아닙니다. ViewModels는 기본 유형으로 캐스팅되었으므로 컴파일 중에 경고없이 런타임 중에 코드를 중단 할 수 있습니다.이 경우 기본 Android 팩토리를 사용하고 이미 인스턴스화 된 뷰 모델에 매개 변수를 전달하는 것이 좋습니다.
mlyko

@mlyko 물론, 이것들은 모두 유효한 반대이며 viewmodel 데이터를 설정하는 자체 방법은 항상 옵션입니다. 그러나 때로는 viewmodel이 초기화되어 생성자를 사용하고 있는지 확인하고 싶습니다. 그렇지 않으면 "뷰 모델이 아직 초기화되지 않음"상황을 직접 처리해야합니다. 예를 들어 viewmodel에 LivedData를 반환하는 메서드가 있고 관찰자가 다양한 View 수명주기 메서드에 연결되어있는 경우입니다.
rzehan

3

@ vilpe89를 기반으로 위의 AndroidViewModel 케이스용 Kotlin 솔루션

class ExtraParamsViewModelFactory(private val application: Application, private val myExtraParam: String): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = SomeViewModel(application, myExtraParam) as T

}

그런 다음 조각은 viewModel을 다음과 같이 시작할 수 있습니다.

class SomeFragment : Fragment() {
 ....
    private val myViewModel: SomeViewModel by viewModels {
        ExtraParamsViewModelFactory(this.requireActivity().application, "some string value")
    }
 ....
}

그리고 실제 ViewModel 클래스는

class SomeViewModel(application: Application, val myExtraParam:String) : AndroidViewModel(application) {
....
}

또는 적절한 방법으로 ...

override fun onActivityCreated(...){
    ....

    val myViewModel = ViewModelProvider(this, ExtraParamsViewModelFactory(this.requireActivity().application, "some string value")).get(SomeViewModel::class.java)

    ....
}

질문은 위의 내용을 따르지 않는 컨텍스트를 사용하지 않고 인수 / 매개 변수를 전달하는 방법을 묻습니다. 애플리케이션 컨텍스트를 제외하고 사용자 지정 AndroidViewModel 생성자에 추가 인수를 전달하는 방법이 있습니까?
Adam Hurwitz

3

이미 생성 된 객체가 전달되는 클래스로 만들었습니다.

private Map<String, ViewModel> viewModelMap;

public ViewModelFactory() {
    this.viewModelMap = new HashMap<>();
}

public void add(ViewModel viewModel) {
    viewModelMap.put(viewModel.getClass().getCanonicalName(), viewModel);
}

@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
    for (Map.Entry<String, ViewModel> viewModel : viewModelMap.entrySet()) {
        if (viewModel.getKey().equals(modelClass.getCanonicalName())) {
            return (T) viewModel.getValue();
        }
    }
    return null;
}

그리고

ViewModelFactory viewModelFactory = new ViewModelFactory();
viewModelFactory.add(new SampleViewModel(arg1, arg2));
SampleViewModel sampleViewModel = ViewModelProviders.of(this, viewModelFactory).get(SampleViewModel.class);

생성자에게 매개 변수를 전달하려면 모든 ViewModel에 대해 ViewModelFactory가 있어야합니다.
K Pradeep Kumar Reddy

아니요. 모든 ViewModel에 대해 단 하나의 ViewModelFactory
Danil

hashMap 키로 정식 이름을 사용하는 이유가 있습니까? class.simpleName을 사용할 수 있습니까?
K Pradeep Kumar Reddy

예,하지만 중복 된 이름이 없는지 확인해야합니다
Danil

이것이 권장되는 코드 작성 스타일입니까? 이 코드를 스스로 생각해 냈거나 안드로이드 문서에서 읽었습니까?
K Pradeep Kumar Reddy

1

Dagger에서 종속성으로 제공 할 수있는 ViewModel 인수로 원활하게 작업하면서이 작업을보다 간단하고 깔끔하게 만들 수있는 라이브러리를 작성했습니다. https://github.com/radutopor/ViewModelFactory

@ViewModelFactory
class UserViewModel(@Provided repository: Repository, userId: Int) : ViewModel() {

    val greeting = MutableLiveData<String>()

    init {
        val user = repository.getUser(userId)
        greeting.value = "Hello, $user.name"
    }    
}

보기에서 :

class UserActivity : AppCompatActivity() {
    @Inject
    lateinit var userViewModelFactory2: UserViewModelFactory2

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)
        appComponent.inject(this)

        val userId = intent.getIntExtra("USER_ID", -1)
        val viewModel = ViewModelProviders.of(this, userViewModelFactory2.create(userId))
            .get(UserViewModel::class.java)

        viewModel.greeting.observe(this, Observer { greetingText ->
            greetingTextView.text = greetingText
        })
    }
}

0

(KOTLIN) 내 솔루션은 약간의 반사를 사용합니다.

인수가 필요한 새 ViewModel 클래스를 만들 때마다 동일한 모양의 Factory 클래스를 만들고 싶지 않다고 가정 해 보겠습니다. Reflection을 통해이 작업을 수행 할 수 있습니다.

예를 들어 두 가지 다른 활동이 있습니다.

class Activity1 : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val args = Bundle().apply { putString("NAME_KEY", "Vilpe89") }
        val viewModel = ViewModelProviders.of(this, ViewModelWithArgumentsFactory(args))
            .get(ViewModel1::class.java)
    }
}

class Activity2 : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val args = Bundle().apply { putInt("AGE_KEY", 29) }
        val viewModel = ViewModelProviders.of(this, ViewModelWithArgumentsFactory(args))
            .get(ViewModel2::class.java)
    }
}

그리고 해당 활동에 대한 ViewModels :

class ViewModel1(private val args: Bundle) : ViewModel()

class ViewModel2(private val args: Bundle) : ViewModel()

그런 다음 마술 부분, Factory 클래스의 구현 :

class ViewModelWithArgumentsFactory(private val args: Bundle) : NewInstanceFactory() {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        try {
            val constructor: Constructor<T> = modelClass.getDeclaredConstructor(Bundle::class.java)
            return constructor.newInstance(args)
        } catch (e: Exception) {
            Timber.e(e, "Could not create new instance of class %s", modelClass.canonicalName)
            throw e
        }
    }
}

0

이렇게하지 않는 이유 :

public class MyViewModel extends AndroidViewModel {
    private final LiveData<List<MyObject>> myObjectList;
    private AppDatabase appDatabase;
    private boolean initialized = false;

    public MyViewModel(Application application) {
        super(application);
    }

    public initialize(String param){
      synchronized ("justInCase") {
         if(! initialized){
          initialized = true;
          appDatabase = AppDatabase.getDatabase(this.getApplication());
          myObjectList = appDatabase.myOjectModel().getMyObjectByParam(param);
    }
   }
  }
}

다음 두 단계로 다음과 같이 사용합니다.

MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)
myViewModel.initialize(param)

2
생성자에 매개 변수를 넣는 요점은 뷰 모델을 한 번만 초기화하는 것 입니다. 당신이 호출하면 구현으로 myViewModel.initialize(param)onCreate활동, 예를 들어, 같은 여러 번 호출 할 수 있습니다 MyViewModel사용자가 장치를 회전 할 때 인스턴스입니다.
Sanlok Lee

@Sanlok Lee Ok. 불필요한 경우 초기화를 방지하기 위해 함수에 조건을 추가하는 것은 어떻습니까? 내 편집 된 답변을 확인하십시오.
아 므르 Berag

0
class UserViewModelFactory(private val context: Context) : ViewModelProvider.NewInstanceFactory() {
 
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return UserViewModel(context) as T
    }
 
}
class UserViewModel(private val context: Context) : ViewModel() {
 
    private var listData = MutableLiveData<ArrayList<User>>()
 
    init{
        val userRepository : UserRepository by lazy {
            UserRepository
        }
        if(context.isInternetAvailable()) {
            listData = userRepository.getMutableLiveData(context)
        }
    }
 
    fun getData() : MutableLiveData<ArrayList<User>>{
        return listData
    }

활동에서 Viewmodel 호출

val userViewModel = ViewModelProviders.of(this,UserViewModelFactory(this)).get(UserViewModel::class.java)

더 많은 참조 : Android MVVM Kotlin 예제


질문은 위의 내용을 따르지 않는 컨텍스트를 사용하지 않고 인수 / 매개 변수를 전달하는 방법을 묻습니다. 애플리케이션 컨텍스트를 제외하고 사용자 지정 AndroidViewModel 생성자에 추가 인수를 전달하는 방법이 있습니까?
Adam Hurwitz

커스텀 viewmodel 생성자에서 모든 인수 / 매개 변수를 전달할 수 있습니다. 여기 컨텍스트는 예일뿐입니다. 생성자에서 모든 사용자 지정 인수를 전달할 수 있습니다.
Dhrumil Shah

알겠습니다. 컨텍스트, 뷰, 활동, 조각, 어댑터, 뷰 라이프 사이클, 뷰 라이프 사이클 인식 관찰 가능 항목을 관찰하거나 뷰가 파괴 될 수 있고 ViewModel이 오래된 상태로 유지되므로 ViewModel에 리소스 (드로어 블 등)를 유지하지 않는 것이 가장 좋습니다. 정보.
Adam Hurwitz
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.