อาร์กิวเมนต์เพิ่มเติมของ Android ViewModel


115

มีวิธีส่งอาร์กิวเมนต์เพิ่มเติมไปยังตัว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ไปยังประเพณีของฉันViewModelได้อย่างไร ฉันสามารถส่งผ่านบริบทของแอปพลิเคชันเท่านั้น แต่ไม่สามารถโต้แย้งเพิ่มเติมได้ ฉันจะขอบคุณทุกความช่วยเหลือ ขอบคุณ.

แก้ไข: ฉันได้เพิ่มรหัสแล้ว ฉันหวังว่าตอนนี้จะดีขึ้น


เพิ่มรายละเอียดและรหัส
hugo

ข้อความแสดงข้อผิดพลาดคืออะไร?
Moses Aprico

ไม่มีข้อความแสดงข้อผิดพลาด ฉันไม่รู้ว่าจะตั้งค่าอาร์กิวเมนต์สำหรับตัวสร้างได้ที่ไหนเนื่องจาก ViewModelProvider ใช้สำหรับสร้างวัตถุ AndroidViewModel
Mario Rudman

คำตอบ:


224

คุณต้องมีคลาสโรงงานสำหรับ 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 ของคุณจะถูกสร้างขึ้นใหม่หลังจากเปลี่ยนแนว คุณไม่สามารถสร้างโรงงานได้ทุกครั้ง
ทิม

3
ที่ไม่เป็นความจริง. ใหม่วิธีการป้องกันไม่ให้สร้างViewModel get()ตามเอกสารประกอบ: "ส่งคืน ViewModel ที่มีอยู่หรือสร้างขึ้นใหม่ในขอบเขต (โดยปกติคือแฟรกเมนต์หรือกิจกรรม) ที่เชื่อมโยงกับ ViewModelProvider นี้" ดู: developer.android.com/reference/android/arch/lifecycle/…
mlyko

2
วิธีการใช้return modelClass.cast(new MyViewModel(mApplication, mParam))เพื่อกำจัดคำเตือน
jackycflau

24

ใช้กับ Dependency Injection

นี่เป็นขั้นสูงและดีกว่าสำหรับรหัสการผลิต

Dagger2ซึ่งเป็นAssistedInjectของ Square นำเสนอการใช้งานที่พร้อมใช้งานจริงสำหรับ ViewModels ที่สามารถฉีดส่วนประกอบที่จำเป็นเช่นที่เก็บข้อมูลที่จัดการคำขอเครือข่ายและฐานข้อมูล นอกจากนี้ยังอนุญาตให้มีการแทรกอาร์กิวเมนต์ / พารามิเตอร์ด้วยตนเองในกิจกรรม / ส่วน นี่คือร่างที่กระชับของขั้นตอนในการดำเนินการกับจิสต์รหัสขึ้นอยู่กับการโพสต์รายละเอียด Gabor Varadi ของกริชเคล็ดลับ

Dagger Hiltเป็นโซลูชันรุ่นต่อไปในเวอร์ชันอัลฟา ณ วันที่ 7/12/20 ซึ่งนำเสนอกรณีการใช้งานแบบเดียวกันพร้อมการตั้งค่าที่ง่ายกว่าเมื่อไลบรารีอยู่ในสถานะเผยแพร่

ใช้งานกับLifecycle 2.2.0ใน Kotlin

การส่งผ่านอาร์กิวเมนต์ / พารามิเตอร์

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

ในขณะที่เอาชนะการสร้างในโรงงานฉันได้รับคำเตือนว่า Unchecked cast 'ItemViewModel to T'
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);

ด้วยโมเดลมุมมองที่แตกต่างกันซึ่งมีตัวสร้างที่แตกต่างกัน


9
ฉันไม่แนะนำวิธีนี้เนื่องจากเหตุผลสองประการ: 1) พารามิเตอร์ในโรงงานไม่ปลอดภัย - ด้วยวิธีนี้คุณสามารถทำลายรหัสของคุณบนรันไทม์ได้ พยายามหลีกเลี่ยงแนวทางนี้เสมอเมื่อเป็นไปได้ 2) การตรวจสอบประเภทโมเดลมุมมองไม่ใช่วิธี OOP ในการทำสิ่งต่างๆ เนื่องจาก ViewModels ถูกแคสต์เป็นประเภทพื้นฐานอีกครั้งคุณจึงสามารถแบ่งโค้ดระหว่างรันไทม์ได้โดยไม่มีคำเตือนใด ๆ ระหว่างการคอมไพล์ในกรณีนี้ฉันขอแนะนำให้ใช้ค่าเริ่มต้นของโรงงาน android และส่งผ่านพารามิเตอร์ไปยังโมเดลมุมมองที่สร้างอินสแตนซ์แล้ว
mlyko

@mlyko แน่นอนว่านี่คือการคัดค้านที่ถูกต้องทั้งหมดและวิธีการของตัวเองในการตั้งค่าข้อมูล viewmodel เป็นตัวเลือกเสมอ แต่บางครั้งคุณต้องการตรวจสอบให้แน่ใจว่ามีการเตรียมใช้งาน viewmodel แล้วดังนั้นจึงต้องใช้ตัวสร้าง มิฉะนั้นคุณจะต้องจัดการกับสถานการณ์ "viewmodel ยังไม่เริ่มต้น" ตัวอย่างเช่นหาก viewmodel มีเมธอดที่ส่งคืน LivedData และผู้สังเกตการณ์ถูกแนบมาด้วยในวิธี View lifecycle ต่างๆ
rzehan

4

อ้างอิงจาก @ vilpe89 โซลูชัน Kotlin ข้างต้นสำหรับเคส 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

}

จากนั้นแฟรกเมนต์สามารถเริ่มต้น 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);

เราควรมี ViewModelFactory สำหรับทุก ViewModel เพื่อส่งผ่านพารามิเตอร์ไปยังคอนสตรัคเตอร์ ??
K Pradeep Kumar Reddy

ไม่เพียง ViewModelFactory เดียวสำหรับ ViewModels ทั้งหมด
Danil

มีเหตุผลใดในการใช้ชื่อบัญญัติเป็นคีย์ hashMap หรือไม่ ฉันสามารถใช้ class.simpleName ได้หรือไม่
K Pradeep Kumar Reddy

ใช่ แต่คุณต้องแน่ใจว่าไม่มีชื่อซ้ำกัน
Danil

นี่เป็นรูปแบบการเขียนโค้ดที่แนะนำหรือไม่? คุณสร้างรหัสนี้ขึ้นมาเองหรืออ่านในเอกสาร Android?
K Pradeep Kumar Reddy

1

ฉันเขียนไลบรารีที่ควรทำให้สิ่งนี้ตรงไปตรงมามากขึ้นและวิธีที่สะอาดขึ้นไม่จำเป็นต้องมีการเชื่อมต่อแบบหลายแถบหรือแบบสำเร็จรูปจากโรงงานในขณะที่ทำงานอย่างราบรื่นกับอาร์กิวเมนต์ ViewModel ที่ 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"
    }    
}

ในมุมมอง:

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) วิธีแก้ปัญหาของฉันใช้ Reflection เล็กน้อย

สมมติว่าคุณไม่ต้องการสร้างคลาส Factory ที่มีลักษณะเหมือนกันทุกครั้งที่คุณสร้างคลาส ViewModel ใหม่ซึ่งต้องการอาร์กิวเมนต์ คุณสามารถทำสิ่งนี้ได้ผ่านการสะท้อนกลับ

ตัวอย่างเช่นคุณจะมีสองกิจกรรมที่แตกต่างกัน:

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

จากนั้นส่วนเวทมนตร์การใช้งานคลาสโรงงาน:

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

1
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 Viewmodel ในกิจกรรม

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

สำหรับการอ้างอิงเพิ่มเติม: ตัวอย่าง Android MVVM Kotlin


คำถามถามว่าจะส่งอาร์กิวเมนต์ / พารามิเตอร์โดยไม่ใช้บริบทที่ไม่เป็นไปตามข้างต้นได้อย่างไร: มีวิธีส่งอาร์กิวเมนต์เพิ่มเติมไปยังตัวสร้าง AndroidViewModel ที่กำหนดเองของฉันยกเว้นบริบทแอปพลิเคชันหรือไม่
Adam Hurwitz

คุณสามารถส่งอาร์กิวเมนต์ / พารามิเตอร์ใดก็ได้ในตัวสร้าง viewmodel ที่กำหนดเองของคุณ บริบทนี้เป็นเพียงตัวอย่างเท่านั้น คุณสามารถส่งผ่านอาร์กิวเมนต์ที่กำหนดเองในตัวสร้าง
Dhrumil Shah

เข้าใจแล้ว. เป็นแนวทางปฏิบัติที่ดีที่สุดที่จะไม่ส่งผ่านบริบทมุมมองกิจกรรมชิ้นส่วนอะแด็ปเตอร์ดู Lifecycle สังเกตดูสิ่งที่สังเกตได้ของวงจรชีวิตหรือเก็บทรัพยากร (drawables ฯลฯ ) ใน ViewModel เนื่องจากมุมมองอาจถูกทำลายและ ViewModel จะยังคงมีอยู่หากล้าสมัย ข้อมูล.
Adam Hurwitz

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

@ สันนอกลีโอเค. วิธีการเพิ่มเงื่อนไขให้กับฟังก์ชันเพื่อป้องกันการเริ่มต้นเมื่อไม่จำเป็น ตรวจสอบคำตอบที่แก้ไขของฉัน
Amr Berag
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.