Đối số bổ sung của Android ViewModel


107

Có cách nào để truyền đối số bổ sung cho hàm tạo tùy chỉnh của tôi AndroidViewModelngoại trừ ngữ cảnh Ứng dụng không. Thí dụ:

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);
    }
}

Và khi tôi muốn sử dụng ViewModellớp tùy chỉnh của mình, tôi sử dụng mã này trong phân đoạn của mình:

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

Vì vậy, tôi không biết cách chuyển đối số bổ sung String paramvào tùy chỉnh của mình ViewModel. Tôi chỉ có thể chuyển ngữ cảnh Ứng dụng chứ không thể chuyển các đối số bổ sung. Tôi thực sự sẽ đánh giá cao bất kỳ sự giúp đỡ nào. Cảm ơn bạn.

Chỉnh sửa: Tôi đã thêm một số mã. Tôi hy vọng nó tốt hơn bây giờ.


thêm chi tiết và mã
hugo

Thông báo lỗi là gì?
Moses Aprico

Không có thông báo lỗi. Tôi chỉ đơn giản là không biết nơi đặt đối số cho hàm tạo vì ViewModelProvider được sử dụng để tạo các đối tượng AndroidViewModel.
Mario Rudman

Câu trả lời:


213

Bạn cần có một lớp nhà máy cho ViewModel của mình.

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);
    }
}

Và khi khởi tạo mô hình xem, bạn làm như sau:

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

Đối với kotlin, bạn có thể sử dụng thuộc tính được ủy quyền:

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

Ngoài ra còn có một tùy chọn mới khác - để thực hiện HasDefaultViewModelProviderFactoryvà ghi đè getDefaultViewModelProviderFactory()với việc khởi tạo nhà máy của bạn và sau đó bạn sẽ gọi ViewModelProvider(this)hoặc by viewModels()không có nhà máy.


4
Có phải mọi ViewModellớp đều cần ViewModelFactory của nó không?
dmlebron

6
nhưng mọi ViewModelcó thể / sẽ có DI khác nhau. Làm thế nào bạn biết được trường hợp nào trả về trên create()phương thức?
dmlebron

1
ViewModel của bạn sẽ được tạo lại sau khi thay đổi hướng. Bạn không thể tạo nhà máy mọi lúc.
Tim

3
Đo không phải sự thật. ViewModelPhương pháp ngăn tạo mới get(). Dựa trên tài liệu: "Trả về ViewModel hiện có hoặc tạo một ViewModel mới trong phạm vi (thường là một đoạn hoặc một hoạt động), được liên kết với ViewModelProvider này." xem: developer.android.com/reference/android/arch/lifecycle/…
mlyko

2
như thế nào về việc sử dụng return modelClass.cast(new MyViewModel(mApplication, mParam))để thoát khỏi cảnh báo
jackycflau

23

Triển khai với Dependency Injection

Điều này cao cấp hơn và tốt hơn cho mã sản xuất.

Dagger2 , AssistedInject của Square cung cấp triển khai sẵn sàng sản xuất cho ViewModels có thể đưa vào các thành phần cần thiết như kho lưu trữ xử lý các yêu cầu mạng và cơ sở dữ liệu. Nó cũng cho phép đưa vào thủ công các đối số / tham số trong hoạt động / phân mảnh. Dưới đây là phác thảo ngắn gọn về các bước triển khai với Code Gists dựa trên bài đăng chi tiết của Gabor Varadi, Dagger Tips .

Dagger Hilt , là giải pháp thế hệ tiếp theo, ở dạng alpha kể từ ngày 7/12/20, cung cấp cùng một trường hợp sử dụng với thiết lập đơn giản hơn khi thư viện ở trạng thái phát hành.

Triển khai với Lifecycle 2.2.0 trong Kotlin

Truyền đối số / tham số

// 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") } 
}

Bật Trạng thái đã lưu với Đối số / Tham số

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

Trong khi ghi đè tạo trong nhà máy, tôi nhận được cảnh báo cho biết Đã bỏ chọn diễn viên 'ItemViewModel to T'
Ssenyonjo

1
Cảnh báo đó không phải là một vấn đề đối với tôi cho đến nay. Tuy nhiên, tôi sẽ xem xét kỹ hơn khi tôi cấu trúc lại nhà máy ViewModel để đưa nó vào sử dụng Dagger thay vì tạo một phiên bản của nó thông qua phân mảnh.
Adam Hurwitz

15

Đối với một nhà máy được chia sẻ giữa nhiều mô hình xem khác nhau, tôi muốn mở rộng câu trả lời của mlyko như sau:

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);
        }
    }
}

Và khởi tạo các mô hình xem:

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);

Với các mô hình xem khác nhau có các hàm tạo khác nhau.


8
Tôi không khuyên bạn nên làm theo cách này vì một số lý do: 1) các tham số trong nhà máy không phải là loại an toàn - bằng cách này, bạn có thể phá vỡ mã của mình trong thời gian chạy. Luôn cố gắng tránh cách tiếp cận này khi có thể 2) kiểm tra các kiểu mô hình chế độ xem không thực sự là cách làm việc OOP. Vì các ViewModels được chuyển sang kiểu cơ sở, một lần nữa bạn có thể ngắt mã trong thời gian chạy mà không có bất kỳ cảnh báo nào trong quá trình biên dịch .. Trong trường hợp này, tôi khuyên bạn nên sử dụng nhà máy android mặc định và chuyển các tham số vào mô hình xem đã được khởi tạo.
mlyko

@mlyko Chắc chắn, đây là tất cả các phản đối hợp lệ và (các) phương pháp riêng để thiết lập dữ liệu mô hình xem luôn là một tùy chọn. Nhưng đôi khi bạn muốn đảm bảo rằng mô hình xem đã được khởi tạo, do đó sử dụng hàm tạo. Nếu không, bạn phải tự xử lý tình huống "view model chưa được khởi tạo". Ví dụ: nếu viewmodel có các phương thức trả về LivedData và các trình quan sát được gắn vào đó trong các phương thức vòng đời View khác nhau.
rzehan

3

Dựa trên @ vilpe89, giải pháp Kotlin ở trên cho các trường hợp AndroidViewModel

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

}

Sau đó, một đoạn có thể bắt đầu viewModel như

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

Và sau đó là lớp ViewModel thực tế

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

Hoặc trong một số phương pháp phù hợp ...

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

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

    ....
}

Câu hỏi hỏi cách truyền đối số / tham số mà không sử dụng ngữ cảnh mà ở trên không tuân theo: Có cách nào để truyền đối số bổ sung cho hàm tạo AndroidViewModel tùy chỉnh của tôi ngoại trừ Ngữ cảnh ứng dụng không?
Adam Hurwitz

3

Tôi đã biến nó thành một lớp trong đó đối tượng đã được tạo sẽ được truyền vào.

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;
}

Và sau đó

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

Chúng ta nên có một ViewModelFactory cho mọi ViewModel để truyền các tham số cho hàm tạo ??
K Pradeep Kumar Reddy

Không. Chỉ một ViewModelFactory cho tất cả ViewModels
Danil

Có lý do gì để sử dụng tên chuẩn làm khóa hashMap không? Tôi có thể sử dụng class.simpleName không?
K Pradeep Kumar Reddy

Có, nhưng bạn phải đảm bảo không có tên trùng lặp
Danil

Đây có phải là phong cách viết mã được khuyến nghị không? Bạn đã tự mình nghĩ ra mã này hay bạn đọc nó trong tài liệu android?
K Pradeep Kumar Reddy

1

Tôi đã viết một thư viện giúp việc này trở nên đơn giản hơn và gọn gàng hơn, không cần nhiều liên kết hoặc bảng soạn sẵn của nhà máy, trong khi làm việc liên tục với các đối số ViewModel có thể được cung cấp dưới dạng phụ thuộc bởi Dagger: 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"
    }    
}

Theo quan điểm:

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
        })
    }
}

1

(KOTLIN) Giải pháp của tôi sử dụng một chút Reflection.

Giả sử bạn không muốn tạo lớp Factory giống nhau mỗi khi bạn tạo lớp ViewModel mới cần một số đối số. Bạn có thể thực hiện điều này thông qua Reflection.

Ví dụ, bạn sẽ có hai Hoạt động khác nhau:

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)
    }
}

Và ViewModels cho các Hoạt động đó:

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

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

Sau đó là phần ma thuật, việc triển khai lớp 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

Tại sao không làm như thế này:

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);
    }
   }
  }
}

và sau đó sử dụng nó như thế này trong hai bước:

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

2
Toàn bộ điểm của việc đưa các tham số vào hàm tạo là khởi tạo mô hình khung nhìn chỉ một lần . Với thực hiện của bạn, nếu bạn gọi myViewModel.initialize(param)trong onCreatecác hoạt động, ví dụ, nó có thể được gọi nhiều lần trên cùng một MyViewModelví dụ như người dùng xoay thiết bị.
Sanlok Lee

@Sanlok Lee Ok. Làm thế nào về việc thêm một điều kiện vào hàm để ngăn chặn việc khởi tạo khi không cần thiết. Kiểm tra câu trả lời đã chỉnh sửa của tôi.
Amr 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
    }

Call Model View in Activity

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

Để tham khảo thêm: Ví dụ về Android MVVM Kotlin


Câu hỏi hỏi cách truyền đối số / tham số mà không sử dụng ngữ cảnh mà ở trên không tuân theo: Có cách nào để truyền đối số bổ sung cho hàm tạo AndroidViewModel tùy chỉnh của tôi ngoại trừ Ngữ cảnh ứng dụng không?
Adam Hurwitz

Bạn có thể truyền bất kỳ đối số / tham số nào trong phương thức khởi tạo mô hình xem tùy chỉnh của mình. Đây chỉ là một ví dụ. Bạn có thể chuyển bất kỳ đối số tùy chỉnh nào trong hàm tạo.
Dhrumil Shah

Hiểu. Cách tốt nhất là không chuyển ngữ cảnh, chế độ xem, hoạt động, phân đoạn, bộ điều hợp, xem Vòng đời, quan sát các quan sát nhận biết vòng đời của chế độ xem hoặc giữ tài nguyên (có thể kéo, v.v.) trong ViewModel vì chế độ xem có thể bị phá hủy và ViewModel sẽ tồn tại với lỗi thời thông tin.
Adam Hurwitz
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.