Cách tải Context trong Android MVVM ViewModel


90

Tôi đang cố gắng triển khai mẫu MVVM trong ứng dụng Android của mình. Tôi đã đọc rằng ViewModels không được chứa mã cụ thể cho android (để giúp việc kiểm tra dễ dàng hơn), tuy nhiên tôi cần sử dụng ngữ cảnh cho nhiều thứ khác nhau (lấy tài nguyên từ xml, khởi tạo tùy chọn, v.v.). Cách tốt nhất để làm việc này là gì? Tôi thấy điều đó AndroidViewModelcó tham chiếu đến ngữ cảnh ứng dụng, tuy nhiên có chứa mã cụ thể của android nên tôi không chắc liệu đó có phải trong ViewModel hay không. Ngoài ra, những sự kiện đó cũng liên kết với các sự kiện trong vòng đời Hoạt động, nhưng tôi đang sử dụng dao găm để quản lý phạm vi của các thành phần nên tôi không chắc điều đó sẽ ảnh hưởng đến nó như thế nào. Tôi mới sử dụng mẫu MVVM và Dagger nên mọi sự trợ giúp đều được đánh giá cao!


Đề phòng trường hợp ai đó đang cố gắng sử dụng AndroidViewModelnhưng nhận được Cannot create instance exceptionthì bạn có thể tham khảo câu trả lời này của tôi stackoverflow.com/a/62626408/1055241
gprathour

Bạn không nên sử dụng Context trong ViewModel, thay vào đó hãy tạo UseCase để lấy Context theo cách đó
Ruben Caster

Câu trả lời:


71

Bạn có thể sử dụng một Applicationngữ cảnh được cung cấp bởi AndroidViewModel, bạn nên mở rộng AndroidViewModelmà chỉ đơn giản là một ngữ cảnh ViewModelbao gồm một Applicationtham chiếu.


Làm việc như người ở!
SPM

Ai đó có thể hiển thị điều này trong mã? Tôi đang ở Java
Biswas Khayargoli

55

Đối với mô hình xem thành phần kiến ​​trúc Android,

Việc chuyển Bối cảnh hoạt động của bạn sang ViewModel của Activity không phải là một phương pháp hay vì nó bị rò rỉ bộ nhớ.

Do đó, để lấy ngữ cảnh trong ViewModel của bạn, lớp ViewModel nên mở rộng Lớp mô hình chế độ xem Android . Bằng cách đó, bạn có thể nhận được ngữ cảnh như được hiển thị trong mã ví dụ bên dưới.

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

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

    //... ViewModel methods 

}

2
Tại sao không sử dụng trực tiếp tham số ứng dụng và một ViewModel bình thường? Tôi thấy không có điểm nào trong "getApplication <Application> ()". Nó chỉ thêm boilerplate.
Điều đáng kinh ngạc

50

Không phải là ViewModels không nên chứa mã cụ thể của Android để làm cho việc kiểm tra dễ dàng hơn, vì nó là phần trừu tượng giúp kiểm tra dễ dàng hơn.

Lý do tại sao ViewModels không nên chứa một phiên bản của Context hoặc bất kỳ thứ gì giống như Views hoặc các đối tượng khác giữ một Context là bởi vì nó có một vòng đời riêng biệt với các Activity và Fragment.

Ý tôi là, giả sử bạn thực hiện thay đổi xoay vòng trên ứng dụng của mình. Điều này khiến Activity và Fragment của bạn tự phá hủy để nó tự tạo lại. ViewModel có nghĩa là vẫn tồn tại trong trạng thái này, vì vậy có khả năng xảy ra sự cố và các trường hợp ngoại lệ khác nếu nó vẫn giữ Chế độ xem hoặc Ngữ cảnh cho Hoạt động bị phá hủy.

Về cách bạn nên làm những gì bạn muốn làm, MVVM và ViewModel hoạt động thực sự tốt với thành phần Databinding của JetPack. Đối với hầu hết những thứ bạn thường lưu trữ Chuỗi, int hoặc v.v., bạn có thể sử dụng Databinding để làm cho Chế độ xem hiển thị trực tiếp, do đó không cần lưu trữ giá trị bên trong ViewModel.

Nhưng nếu bạn không muốn Databinding, bạn vẫn có thể truyền Context bên trong hàm tạo hoặc các phương thức để truy cập Tài nguyên. Chỉ cần không giữ một phiên bản của Ngữ cảnh đó bên trong ViewModel của bạn.


1
Tôi hiểu rằng việc bao gồm mã dành riêng cho android yêu cầu chạy các bài kiểm tra thiết bị đo, chậm hơn nhiều so với các bài kiểm tra JUnit thông thường. Tôi hiện đang sử dụng Databinding cho các phương pháp nhấp chuột nhưng tôi không biết nó sẽ giúp ích như thế nào khi lấy tài nguyên từ xml hoặc cho các tùy chọn. Tôi chỉ nhận ra rằng đối với các sở thích, tôi cũng sẽ cần bối cảnh bên trong mô hình của mình. Những gì tôi hiện đang làm là có Dagger bơm bối cảnh ứng dụng (module bối cảnh được nó từ một phương pháp tĩnh bên trong lớp ứng dụng)
Vincent Williams

@VincentWilliams Có, việc sử dụng ViewModel giúp trừu tượng hóa mã khỏi các thành phần giao diện người dùng của bạn, giúp bạn tiến hành kiểm tra dễ dàng hơn. Nhưng, những gì tôi đang nói là lý do chính để không bao gồm bất kỳ Ngữ cảnh, Chế độ xem hoặc những thứ tương tự không phải vì lý do thử nghiệm, mà là vì vòng đời của ViewModel có thể giúp bạn tránh sự cố và các lỗi khác. Đối với databinding, điều này có thể giúp bạn về tài nguyên vì hầu hết thời gian bạn cần truy cập vào tài nguyên trong mã là do cần áp dụng Chuỗi, màu sắc, dimen đó vào bố cục của bạn, điều mà databinding có thể thực hiện trực tiếp.
Jackey

Ồ được rồi, tôi hiểu ý bạn nhưng việc kết hợp dữ liệu sẽ không giúp tôi trong trường hợp này vì tôi cần truy cập các chuỗi để sử dụng trong mô hình (Những chuỗi này có thể được đặt trong lớp hằng số thay vì xml, tôi cho là) ​​và cũng để khởi tạo SharedPreferences
Vincent Williams

3
nếu tôi muốn chuyển đổi văn bản trong một chế độ xem văn bản dựa trên một mô hình xem biểu mẫu giá trị, thì chuỗi đó cần được bản địa hóa, vì vậy tôi cần lấy tài nguyên trong mô hình xem của mình, nếu không có ngữ cảnh thì tôi sẽ truy cập tài nguyên như thế nào?
Srishti Roy

3
@SrishtiRoy Nếu bạn sử dụng databinding, bạn có thể dễ dàng chuyển đổi văn bản của TextView dựa trên giá trị từ mô hình xem của bạn. Không cần truy cập vào Ngữ cảnh bên trong ViewModel của bạn vì tất cả điều này xảy ra trong các tệp bố cục. Tuy nhiên, nếu bạn phải sử dụng Ngữ cảnh trong ViewModel của mình, thì bạn nên cân nhắc sử dụng AndroidViewModel thay vì ViewModel. AndroidViewModel chứa Bối cảnh ứng dụng mà bạn có thể gọi bằng getApplication (), do đó sẽ đáp ứng nhu cầu về Ngữ cảnh của bạn nếu ViewModel của bạn yêu cầu một ngữ cảnh.
Jackey

15

Câu trả lời ngắn gọn - Đừng làm điều này

Tại sao ?

Nó đánh bại toàn bộ mục đích của các mô hình chế độ xem

Hầu hết mọi thứ bạn có thể làm trong mô hình chế độ xem đều có thể được thực hiện trong hoạt động / phân đoạn bằng cách sử dụng các phiên bản LiveData và nhiều cách tiếp cận được đề xuất khác.


21
Tại sao lại tồn tại lớp AndroidViewModel?
Alex Berdnikov 19/09/19

1
@AlexBerdnikov Mục đích của MVVM là cô lập chế độ xem (Hoạt động / Phân mảnh) khỏi ViewModel thậm chí nhiều hơn MVP. Vì vậy, nó sẽ dễ dàng hơn để kiểm tra.
hushing_voice

3
@free_style Cảm ơn bạn đã làm rõ, nhưng câu hỏi vẫn còn tồn tại: nếu chúng ta không giữ ngữ cảnh trong ViewModel, tại sao lớp AndroidViewModel thậm chí còn tồn tại? Toàn bộ mục đích của nó là cung cấp ngữ cảnh ứng dụng, phải không?
Alex Berdnikov

6
@AlexBerdnikov Sử dụng bối cảnh Hoạt động bên trong mô hình xem có thể gây rò rỉ bộ nhớ. Vì vậy, bằng cách sử dụng AndroidViewModel Class, bạn sẽ được cung cấp bởi Application Context mà (hy vọng) sẽ không gây ra bất kỳ rò rỉ bộ nhớ nào. Vì vậy, sử dụng AndroidViewModel có thể tốt hơn là chuyển ngữ cảnh hoạt động cho nó. Nhưng vẫn làm như vậy sẽ gây khó khăn cho việc kiểm tra. Đây là quan điểm của tôi về nó.
hushing_voice

1
Tôi không thể truy cập tệp từ thư mục res / raw từ kho lưu trữ?
Fugogugo

14

Những gì tôi đã làm thay vì có một Ngữ cảnh trực tiếp trong ViewModel, tôi đã tạo các lớp trình cung cấp như ResourceProvider sẽ cung cấp cho tôi các tài nguyên tôi cần và tôi đã đưa các lớp trình cung cấp đó vào ViewModel của mình


1
Tôi đang sử dụng ResourcesProvider với Dagger trong AppModule. Cách tiếp cận tốt để lấy ngữ cảnh là ResourcesProvider hay AndroidViewModel tốt hơn để lấy ngữ cảnh cho tài nguyên?
Usman Rana

@Vincent: Làm cách nào để sử dụng resourceProvider để có được Drawable bên trong ViewModel?
HoangVu 27/1218

@Vegeta Bạn sẽ thêm một phương thức giống như getDrawableRes(@DrawableRes int id)bên trong lớp ResourceProvider
Vincent Williams

1
Điều này đi ngược lại với cách tiếp cận Kiến trúc sạch nói rằng các phụ thuộc khung không được vượt qua ranh giới thành logic miền (ViewModels).
IgorGanapolsky

1
@IgorGanapolsky VM không chính xác là logic miền. Logic miền là các lớp khác như tương tác và kho lưu trữ để đặt tên cho một vài. Máy ảo thuộc loại "keo" vì chúng tương tác với miền của bạn, nhưng không trực tiếp. Nếu máy ảo của bạn là một phần trong miền của bạn thì bạn nên xem xét lại cách bạn đang sử dụng mẫu vì bạn đang giao cho họ quá nhiều trách nhiệm.
mradzinski

8

TL; DR: Đưa ngữ cảnh của Ứng dụng thông qua Dagger trong ViewModels của bạn và sử dụng nó để tải các tài nguyên. Nếu bạn cần tải hình ảnh, hãy chuyển thể hiện View thông qua các đối số từ các phương thức Databinding và sử dụng ngữ cảnh View đó.

MVVM là một kiến ​​trúc tốt và nó chắc chắn là tương lai của sự phát triển Android, nhưng có một số thứ vẫn còn xanh. Lấy ví dụ về giao tiếp lớp trong kiến ​​trúc MVVM, tôi đã thấy các nhà phát triển khác nhau (các nhà phát triển rất nổi tiếng) sử dụng LiveData để giao tiếp các lớp khác nhau theo những cách khác nhau. Một số người trong số họ sử dụng LiveData để giao tiếp ViewModel với UI, nhưng sau đó họ sử dụng giao diện gọi lại để giao tiếp với Kho lưu trữ hoặc họ có Tương tác / Cơ sở sử dụng và họ sử dụng LiveData để giao tiếp với chúng. Vấn đề ở đây, là không phải tất cả mọi thứ là 100% xác định được nêu ra .

Điều đó đang được nói, cách tiếp cận của tôi với vấn đề cụ thể của bạn là có sẵn ngữ cảnh của Ứng dụng thông qua DI để sử dụng trong ViewModels của tôi để lấy những thứ như Chuỗi từ string.xml của tôi

Nếu tôi đang xử lý việc tải hình ảnh, tôi cố gắng chuyển qua các đối tượng View từ các phương thức của bộ điều hợp Databinding và sử dụng ngữ cảnh của View để tải hình ảnh. Tại sao? vì một số công nghệ (ví dụ: Glide) có thể gặp sự cố nếu bạn sử dụng ngữ cảnh của Ứng dụng để tải hình ảnh.

Hy vọng nó giúp!


5
TL; DR nên ở đầu
Jacques Koorts

1
Cảm ơn về câu trả lời của bạn. Tuy nhiên, tại sao bạn lại sử dụng dagger để đưa ngữ cảnh vào nếu bạn có thể mở rộng mô hình xem của mình từ androidviewmodel và sử dụng ngữ cảnh nội trang mà chính lớp đó cung cấp? Đặc biệt là xem xét số lượng vô lý mã boilerplate để làm cho dao găm và MVVM hoạt động cùng nhau, giải pháp khác có vẻ rõ ràng hơn nhiều. Suy nghĩ của bạn về điều này là gì?
Josip Domazet

7

Như những người khác đã đề cập, AndroidViewModelbạn có thể lấy Contextcái gì để có được ứng dụng nhưng từ những gì tôi thu thập được trong các nhận xét, bạn đang cố gắng thao túng @drawablecác s từ bên trong của mình ViewModelđể đánh bại mục đích MVVM.

Nói chung, nhu cầu có một Contexttrong ViewModelhầu hết các gợi ý rằng bạn nên xem xét suy nghĩ lại cách bạn phân chia logic giữa Views và của bạn ViewModels.

Thay vì ViewModelgiải quyết các phần có thể kéo và đưa chúng vào Hoạt động / Phân mảnh, hãy cân nhắc để Phân đoạn / Hoạt động sắp xếp các phần có thể kéo dựa trên dữ liệu được sở hữu bởi ViewModel. Giả sử, bạn cần các bảng có thể kéo khác nhau được hiển thị trong chế độ xem cho trạng thái bật / tắt - đó là trạng thái ViewModelsẽ giữ trạng thái (có thể là boolean) nhưng việc Viewcủa bạn là chọn có thể kéo cho phù hợp.

Nó có thể được thực hiện khá dễ dàng với DataBinding :

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

Nếu bạn có nhiều trạng thái và có thể kéo hơn, để tránh logic khó sử dụng trong tệp bố cục, bạn có thể viết BindingAdapter tùy chỉnh để dịch, chẳng hạn như, một Enumgiá trị thành R.drawable.*(ví dụ: phù hợp với thẻ)

Hoặc có thể bạn cần Contextmột thành phần nào đó mà bạn sử dụng bên trong ViewModel- sau đó, tạo thành phần bên ngoài ViewModelvà chuyển nó vào. Bạn có thể sử dụng DI, hoặc singletons, hoặc tạo Contextthành phần-phụ thuộc ngay trước khi khởi tạo ViewModeltrong Fragment/ Activity.

Tại sao phải bận tâm: Contextlà một thứ dành riêng cho Android và phụ thuộc vào những thứ đó ViewModellà một thực tế không tốt: chúng cản trở việc kiểm thử đơn vị. Mặt khác, các giao diện thành phần / dịch vụ của riêng bạn hoàn toàn nằm trong tầm kiểm soát của bạn nên bạn có thể dễ dàng mô phỏng chúng để thử nghiệm.


5

có tham chiếu đến ngữ cảnh ứng dụng, tuy nhiên có chứa mã cụ thể của android

Tin tốt là bạn có thể sử dụng Mockito.mock(Context.class)và làm cho ngữ cảnh trả về bất cứ thứ gì bạn muốn trong các thử nghiệm!

Vì vậy, chỉ cần sử dụng một ViewModelnhư bình thường và cung cấp cho nó ApplicationContext thông qua ViewModelProviders.Factory như bạn thường làm.


3

bạn có thể truy cập ngữ cảnh ứng dụng getApplication().getApplicationContext()từ trong ViewModel. Đây là những gì bạn cần để truy cập tài nguyên, tùy chọn, v.v.


Tôi đoán để thu hẹp câu hỏi của mình. Có tệ không khi có một tham chiếu ngữ cảnh bên trong mô hình xem (điều này không ảnh hưởng đến việc kiểm tra?) Và việc sử dụng lớp AndroidViewModel có ảnh hưởng đến Dagger theo bất kỳ cách nào không? Nó không bị ràng buộc với vòng đời hoạt động? Tôi đang sử dụng Dagger để kiểm soát vòng đời của các thành phần
Vincent Williams

14
Các ViewModellớp học không có getApplicationphương pháp.
beroal

4
Không, nhưng AndroidViewModel
4Oh 4

1
Nhưng bạn cần phải vượt qua các ví dụ ứng dụng trong constructor của nó, nó chỉ giống như truy cập vào các ví dụ ứng dụng từ nó
John Sardinha

2
Việc có ngữ cảnh ứng dụng không thành vấn đề lớn. Bạn không muốn có bối cảnh hoạt động / phân đoạn bởi vì bạn bị chặn nếu phân đoạn / hoạt động bị phá hủy và mô hình chế độ xem vẫn có tham chiếu đến ngữ cảnh hiện không tồn tại. Nhưng bạn sẽ không bao giờ có ngữ cảnh APPLICATION bị phá hủy nhưng VM vẫn có tham chiếu đến nó. Đúng? Bạn có thể tưởng tượng một tình huống mà ứng dụng của bạn thoát ra nhưng Viewmodel thì không? :)
user1713450

3

Bạn không nên sử dụng các đối tượng liên quan đến Android trong ViewModel của mình vì động cơ của việc sử dụng ViewModel là để tách mã java và mã Android để bạn có thể kiểm tra logic nghiệp vụ của mình một cách riêng biệt và bạn sẽ có một lớp riêng biệt gồm các thành phần Android và logic kinh doanh của mình và dữ liệu, Bạn không nên có ngữ cảnh trong ViewModel của mình vì nó có thể dẫn đến sự cố


2
Đây là một quan sát công bằng, nhưng một số thư viện phụ trợ vẫn yêu cầu bối cảnh Ứng dụng, chẳng hạn như MediaStore. Câu trả lời của 4gus71n dưới đây giải thích cách thỏa hiệp.
Bryan W. Wagner

1
Có, Bạn có thể sử dụng Ngữ cảnh ứng dụng nhưng không phải ngữ cảnh hoạt động, vì ngữ cảnh ứng dụng tồn tại trong suốt vòng đời ứng dụng nhưng không phải ngữ cảnh hoạt động vì việc chuyển ngữ cảnh hoạt động cho bất kỳ quy trình không đồng bộ nào có thể dẫn đến rò rỉ bộ nhớ. Ngữ cảnh được đề cập trong bài đăng của tôi là Hoạt động Context. Nhưng bạn vẫn nên cẩn thận không chuyển ngữ cảnh cho bất kỳ quá trình không đồng bộ nào ngay cả khi đó là ngữ cảnh ứng dụng.
Rohit Sharma

2

Tôi đã gặp sự cố SharedPreferenceskhi sử dụng ViewModellớp học vì vậy tôi đã lấy lời khuyên từ các câu trả lời ở trên và thực hiện cách sử dụng sau AndroidViewModel. Mọi thứ bây giờ trông tuyệt vời

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

}

Và trong 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

Tôi đã tạo nó theo cách này:

@Module
public class ContextModule {

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

Và sau đó tôi vừa thêm vào AppComponent ContextModule.class:

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

Và sau đó tôi đã chèn ngữ cảnh vào ViewModel của mình:

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

0

Sử dụng mẫu sau:

class NameViewModel(
val variable:Class,application: Application):AndroidViewModel(application){
   body...
}
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.