Android Jetpack Series - Shared ViewModel that implements Application scope for mutual communication between Activity and Fragment

Table of contents

foreword

Figure out the nature of activityViewModels

Define the ViewMode of the Application scope

ApplicationViewModelLazy

BaseViewModelApplication

Instructions

Custom Application inherits from BaseViewModelApplication

applicationViewModels gets the ViewModel instance

summary


foreword

It is mentioned in the official document that two or more Fragments in an Activity communicate with each other using ShareViewModel, and obtain the ViewModelStore of the same Activity through the extension function activityViewModels, so as to obtain an instance of the same ViewModel stored in the Activity.

The specific use is as follows:

class SharedViewModel : ViewModel() {
    val selected = MutableLiveData<Item>()

    fun select(item: Item) {
        selected.value = item
    }
}

class ListFragment : Fragment() {

    private lateinit var itemSelector: Selector

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        itemSelector.setOnClickListener { item ->
            // Update the UI
        }
    }
}

class DetailFragment : Fragment() {

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
            // Update the UI
        })
    }
}

ListFragment and DetailFragment get the same instance through activityViewModels, so they can communicate with each other. The above is a solution for two or more Fragments to communicate with each other. What if the Activity and Fragment communicate with each other? According to this idea, can we implement an Application-level ViewModel? The answer is yes!

Figure out the nature of activityViewModels

Before following the gourd, you must figure out how activityViewModels is implemented. After tracking into the source code, you find that activityViewModels is an extension function defined in the FragmentViewModelLazy.kt file:

/**
 * Returns a property delegate to access [ViewModel] by **default** scoped to this [Fragment]:
 * ```
 * class MyFragment : Fragment() {
 *     val viewmodel: MYViewModel by viewmodels()
 * }
 * ```
 *
 * Custom [ViewModelProvider.Factory] can be defined via [factoryProducer] parameter,
 * factory returned by it will be used to create [ViewModel]:
 * ```
 * class MyFragment : Fragment() {
 *     val viewmodel: MYViewModel by viewmodels { myFactory }
 * }
 * ```
 *
 * Default scope may be overridden with parameter [ownerProducer]:
 * ```
 * class MyFragment : Fragment() {
 *     val viewmodel: MYViewModel by viewmodels ({requireParentFragment()})
 * }
 * ```
 *
 * This property can be accessed only after this Fragment is attached i.e., after
 * [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException.
 */
@MainThread
inline fun <reified VM : ViewModel> Fragment.viewModels(
    noinline ownerProducer: () -> ViewModelStoreOwner = { this },
    noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)

/**
 * Returns a property delegate to access parent activity's [ViewModel],
 * if [factoryProducer] is specified then [ViewModelProvider.Factory]
 * returned by it will be used to create [ViewModel] first time. Otherwise, the activity's
 * [androidx.activity.ComponentActivity.getDefaultViewModelProviderFactory](default factory)
 * will be used.
 *
 * ```
 * class MyFragment : Fragment() {
 *     val viewmodel: MyViewModel by activityViewModels()
 * }
 * ```
 *
 * This property can be accessed only after this Fragment is attached i.e., after
 * [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException.
 */
@MainThread
inline fun <reified VM : ViewModel> Fragment.activityViewModels(
    noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { requireActivity().viewModelStore },
    factoryProducer ?: { requireActivity().defaultViewModelProviderFactory })

/**
 * Helper method for creation of [ViewModelLazy], that resolves `null` passed as [factoryProducer]
 * to default factory.
 */
@MainThread
fun <VM : ViewModel> Fragment.createViewModelLazy(
    viewModelClass: KClass<VM>,
    storeProducer: () -> ViewModelStore,
    factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        defaultViewModelProviderFactory
    }
    return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
}

Although the amount of code is very small, let's take out the core skeleton and clear the idea regardless of the details.

You can see that activityViewModels returns the ViewModel instance created by the createViewModelLazy method, createViewModelLazy calls the ViewModelLazy instance, and ViewModelLazy obtains the ViewModel instance through the get method of the ViewModelProvider instance.

figure 1

Figure 1 has relatively completely shown the creation process of the ViewModel instance. But there is a question, why do different Fragments get the same ViewModel instance? To answer this question, we conduct reverse analysis according to Figure 1.

First, the ViewModel instance is created by the ViewModelProvider.Factory factory class:

public open class ViewModelProvider(
    private val store: ViewModelStore,
    private val factory: Factory
) {
    /**
     * Implementations of `Factory` interface are responsible to instantiate ViewModels.
     */
    public interface Factory {
        /**
         * Creates a new instance of the given `Class`.
         *
         * @param modelClass a `Class` whose instance is requested
         * @return a newly created ViewModel
         */
        public fun <T : ViewModel> create(modelClass: Class<T>): T
    }
}


/**
 * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
 * an activity), associated with this `ViewModelProvider`.
 *
 * The created ViewModel is associated with the given scope and will be retained
 * as long as the scope is alive (e.g. if it is an activity, until it is
 * finished or process is killed).
 *
 * @param key        The key to use to identify the ViewModel.
 * @param modelClass The class of the ViewModel to create an instance of it if it is not
 * present.
 * @return A ViewModel that is an instance of the given type `T`.
 */
@Suppress("UNCHECKED_CAST")
@MainThread
public open operator fun <T : ViewModel> get(key: String, modelClass: Class<T>): T {
    var viewModel = store[key]
    if (modelClass.isInstance(viewModel)) {
        (factory as? OnRequeryFactory)?.onRequery(viewModel)
        return viewModel as T
    } else {
        @Suppress("ControlFlowWithEmptyBody")
        if (viewModel != null) {
            // TODO: log a warning.
        }
    }
    viewModel = if (factory is KeyedFactory) {
        factory.create(key, modelClass)
    } else {
        factory.create(modelClass)
    }
    store.put(key, viewModel)
    return viewModel
}

It can be seen that the get method will first determine whether the ViewModel instance already exists, and return it directly if it exists, otherwise it will create a ViewModel instance through the factory class Factory and store it in the instance store of ViewModelStore. In this way, as long as the instance of ViewModelStore is the same, the get method of ViewModelProvider returns the same ViewModel instance every time.

Let's look at where the store of ViewModelProvider is obtained from. The get method of ViewModelProvider is called in ViewModelLazy. See the source code of ViewModelLazy:

/**
 * An implementation of [Lazy] used by [androidx.fragment.app.Fragment.viewModels] and
 * [androidx.activity.ComponentActivity.viewmodels].
 *
 * [storeProducer] is a lambda that will be called during initialization, [VM] will be created
 * in the scope of returned [ViewModelStore].
 *
 * [factoryProducer] is a lambda that will be called during initialization,
 * returned [ViewModelProvider.Factory] will be used for creation of [VM]
 */
public class ViewModelLazy<VM : ViewModel> (
    private val viewModelClass: KClass<VM>,
    private val storeProducer: () -> ViewModelStore,
    private val factoryProducer: () -> ViewModelProvider.Factory
) : Lazy<VM> {
    private var cached: VM? = null

    override val value: VM
        get() {
            val viewModel = cached
            return if (viewModel == null) {
                val factory = factoryProducer()
                val store = storeProducer()
                ViewModelProvider(store, factory).get(viewModelClass.java).also {
                    cached = it
                }
            } else {
                viewModel
            }
        }

    override fun isInitialized(): Boolean = cached != null
}

It can be seen that the store is returned by the parameter storeProducer (higher-order functions can be passed as parameters) in the constructor of ViewModelLazy.

And ViewModelLazy is used in createViewModelLazy of FragmentViewModelLazy.kt:

@MainThread
inline fun <reified VM : ViewModel> Fragment.activityViewModels(
    noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { requireActivity().viewModelStore },
    factoryProducer ?: { requireActivity().defaultViewModelProviderFactory })

/**
 * Helper method for creation of [ViewModelLazy], that resolves `null` passed as [factoryProducer]
 * to default factory.
 */
@MainThread
fun <VM : ViewModel> Fragment.createViewModelLazy(
    viewModelClass: KClass<VM>,
    storeProducer: () -> ViewModelStore,
    factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        defaultViewModelProviderFactory
    }
    return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
}

It can be seen that storeProducer is obtained through requireActivity().viewModelStore in the extension function activityViewModels. As long as these Fragments are the same host Activity, the obtained viewModelStore is the same, and finally the ViewModel instance obtained through activityViewModels is also the same.

Define the ViewMode of the Application scope

According to the above analysis, we can implement the shared ViewModel of the Application scope for the mutual communication between the Activity and the Fragment.

ApplicationViewModelLazy

First create an ApplicationViewModelLazy.kt file with the following content:

/**
 * Returns a property delegate to access application's [ViewModel],
 * if [factoryProducer] is specified then [ViewModelProvider.Factory]
 * returned by it will be used to create [ViewModel] first time. Otherwise, the BaseViewModelApplication's
 * [com.nxg.mvvm.BaseViewModelApplication.getDefaultViewModelProviderFactory](default factory)
 * will be used.
 *
 * ```
 * class MyAppCompatActivity : AppCompatActivity() {
 *     val viewmodel: MyViewModel by applicationViewModels()
 * }
 * ```
 *
 * This property can be accessed only after this AppCompatActivity is create i.e., after
 * [AppCompatActivity.onCreate()], and access prior to that will result in IllegalArgumentException.
 */
@MainThread
inline fun <reified VM : ViewModel> AppCompatActivity.applicationViewModels(
    noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
) = createViewModelLazy(VM::class, { (applicationContext as BaseViewModelApplication).viewModelStore },
    factoryProducer ?: { (applicationContext as BaseViewModelApplication).defaultViewModelProviderFactory })
/**
 * Helper method for creation of [ViewModelLazy], that resolves `null` passed as [factoryProducer]
 * to default factory.
 */
@MainThread
fun <VM : ViewModel> AppCompatActivity.createViewModelLazy(
    viewModelClass: KClass<VM>,
    storeProducer: () -> ViewModelStore,
    factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        (applicationContext as BaseViewModelApplication).defaultViewModelProviderFactory
    }
    return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
}

/**
 * Returns a property delegate to access application's [ViewModel],
 * if [factoryProducer] is specified then [ViewModelProvider.Factory]
 * returned by it will be used to create [ViewModel] first time. Otherwise, the activity's
 * [com.nxg.mvvm.BaseViewModelApplication.getDefaultViewModelProviderFactory](default factory)
 * will be used.
 *
 * ```
 * class MyFragment : Fragment() {
 *     val viewmodel: MyViewModel by applicationViewModels()
 * }
 * ```
 *
 * This property can be accessed only after this Fragment is attached i.e., after
 * [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException.
 */
@MainThread
inline fun <reified VM : ViewModel> Fragment.applicationViewModels(
    noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
) = createViewModelLazy(VM::class, { (requireActivity().applicationContext as BaseViewModelApplication).viewModelStore },
    factoryProducer ?: { (requireActivity().applicationContext as BaseViewModelApplication).defaultViewModelProviderFactory })

/**
 * Helper method for creation of [ViewModelLazy], that resolves `null` passed as [factoryProducer]
 * to default factory.
 */
@MainThread
fun <VM : ViewModel> Fragment.createViewModelLazy(
    viewModelClass: KClass<VM>,
    storeProducer: () -> ViewModelStore,
    factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        (requireActivity().applicationContext as BaseViewModelApplication).defaultViewModelProviderFactory
    }
    return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
}

Two extension methods are defined: AppCompatActivity.applicationViewModels and Fragment.applicationViewModels. The usage is the same as activityViewModels, but the viewModelStore provided is different. You can see that the viewModelStore of applicationViewModels is provided by BaseViewModelApplication.

BaseViewModelApplication

The BaseViewModelApplication code is as follows:

open class BaseViewModelApplication : Application(), ViewModelStoreOwner,HasDefaultViewModelProviderFactory{

    companion object {
        const val TAG = "BaseViewModelApplication"
    }

    // Lazily recreated from NonConfigurationInstances by getViewModelStore()
    private var mViewModelStore: ViewModelStore? = null
    private var mDefaultFactory: ViewModelProvider.Factory? = null


    /**
     * Returns the [ViewModelStore] associated with this application
     *
     *
     * Overriding this method is no longer supported and this method will be made
     * `final` in a future version of ComponentActivity.
     *
     * @return a `ViewModelStore`
     * @throws IllegalStateException if called before the Activity is attached to the Application
     * instance i.e., before onCreate()
     */
    @NonNull
    @Override
    override fun getViewModelStore(): ViewModelStore {
        ensureViewModelStore()
        return mViewModelStore as ViewModelStore
    }

    /**
     * Application不需要处理配置改变导致的重建
     */
    private fun  /* synthetic access */ensureViewModelStore() {
        if (mViewModelStore == null) {
            mViewModelStore = ViewModelStore()
        }
    }

    override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
        if(mDefaultFactory == null){
            mDefaultFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(this)
        }
       return mDefaultFactory as ViewModelProvider.Factory
    }
}

BaseViewModelApplication implements ViewModelStoreOwner and HasDefaultViewModelProviderFactory interfaces to provide ViewModelStore and ViewModelProvider.Factory instances. In this way, AppCompatActivity.applicationViewModels and Fragment.applicationViewModels get the same ViewModel instance stored in ViewModelStore in BaseViewModelApplication.

Instructions

Custom Application inherits from BaseViewModelApplication

In the app module, the custom Application inherits from BaseViewModelApplication.

@HiltAndroidApp
class App : BaseViewModelApplication() {

    companion object {
        const val TAG = "AppApplication"
        private var INSTANCE: App by NotNullSingleValueVar()
        fun instance() = INSTANCE
    }

    //定义一个属性管理类,进行非空和重复赋值的判断
    private class NotNullSingleValueVar<T> : ReadWriteProperty<Any?, T> {
        private var value: T? = null
        override fun getValue(thisRef: Any?, property: KProperty<*>): T {
            return value ?: throw IllegalStateException("application not initialized")
        }

        override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
            this.value = if (this.value == null) value
            else throw IllegalStateException("application already initialized")
        }
    }


    override fun onCreate() {
        super.onCreate()
        INSTANCE = this
        Utils.init(this)
        LogUtil.enable = BuildConfig.DEBUG
        registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
    }


    override fun onLowMemory() {
        super.onLowMemory()
        Log.i(
            TAG,
            "onLowMemory: "
        )
    }

    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        Log.i(
            TAG,
            "onTrimMemory: "
        )
    }

    private val mActivityLifecycleCallbacks: ActivityLifecycleCallbacks =
        object : ActivityLifecycleCallbacks {
            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
                Log.i(
                    TAG,
                    "onActivityCreated: " + activity::class.java.name
                )
            }

            override fun onActivityStarted(activity: Activity) {
                Log.i(
                    TAG,
                    "onActivityStarted: " + activity::class.java.name
                )
            }

            override fun onActivityResumed(activity: Activity) {
                Log.i(
                    TAG,
                    "onActivityResumed: " + activity::class.java.name
                )
            }

            override fun onActivityPaused(activity: Activity) {
                Log.i(
                    TAG,
                    "onActivityPaused: " + activity::class.java.name
                )
            }

            override fun onActivityStopped(activity: Activity) {
                Log.i(
                    TAG,
                    "onActivityStopped: " + activity::class.java.name
                )
            }

            override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
                Log.i(
                    TAG,
                    "onActivitySaveInstanceState: " + activity::class.java.name
                )
            }

            override fun onActivityDestroyed(activity: Activity) {
                Log.i(
                    TAG,
                    "onActivityDestroyed: " + activity::class.java.name
                )
            }

        }
}

Note, don't forget to call super.onCreate() when rewriting the onCreate method.

applicationViewModels gets the ViewModel instance

Suppose there is a ViewModel called AppShareViewModel:

/**
 * 作用域范围为Application的共享ShareViewModel
 * 使用方法:
 * ```
 * class MyAppCompatActivity : AppCompatActivity() {
 *     val appShareViewModel: AppShareViewModel by applicationViewModels()
 * }
 *
 * class MyFragment : Fragment() {
 *     val appShareViewModel: AppShareViewModel by applicationViewModels()
 * }
 * ```
 */
class AppShareViewModel(application: Application) : AndroidViewModel(application) {

    // Backing property to avoid state updates from other classes
    private val _uiState = MutableStateFlow(UiState.HOME)

    // The UI collects from this StateFlow to get its state updates
    val uiState: StateFlow<UiState> = _uiState

    init {
        viewModelScope.launch {
            _uiState.value = UiState.HOME
        }
    }

    fun setUiState(state: UiState) {
        viewModelScope.launch {
            _uiState.value = state
        }
    }

    /**
     * 界面状态
     */
    enum class UiState {
        PERMISSION,HOME, START, PAUSE, PLAY
    }

}

Get the instance of AppShareViewModel in Activity and Fragment as follows (support Hilt):

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    companion object {
        const val TAG = "MainActivity"
    }

    private lateinit var binding: MainActivityBinding
	//作用域范围为Application的共享ShareViewModel
    private val appShareViewModel: AppShareViewModel by applicationViewModels()

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

        binding = MainActivityBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Start a coroutine in the lifecycle scope
        lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // Note that this happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                appShareViewModel.uiState.collect {
                    // New value received
                    when (it) {
                        AppShareViewModel.UiState.PERMISSION -> {
                            
                        }
                        AppShareViewModel.UiState.HOME -> {
                           
                        }
                        AppShareViewModel.UiState.START -> {
                            
                        }
                        AppShareViewModel.UiState.PAUSE -> {
                            
                        }
                        AppShareViewModel.UiState.PLAY -> {
                          
                        }

                    }
                }
            }
        }
    }
}

@AndroidEntryPoint
class AudioRecordFragment : Fragment() {

    companion object {
        const val TAG = "AudioRecordFragment"
        fun newInstance() = AudioRecordFragment()
    }
	//作用域范围为Application的共享ShareViewModel
    private val appShareViewModel: AppShareViewModel by applicationViewModels()
    
    //作用域范围为Activity的共享ShareViewModel
    private val shareViewModel:ShareViewModel by activityViewModels()

    private val audioRecordViewModel: AudioRecordViewModel by viewModels()

	private var _binding: AudioRecordFragmentBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = AudioRecordFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
 		......
        appShareViewModel.uiState.collect {
            // New value received
            when (it) {
                AppShareViewModel.UiState.PERMISSION -> {

                }
                AppShareViewModel.UiState.HOME -> {

                }
                AppShareViewModel.UiState.START -> {

                }
                AppShareViewModel.UiState.PAUSE -> {

                }
                AppShareViewModel.UiState.PLAY -> {

                }

            }
        }
    }

}


@AndroidEntryPoint
class AudioRecordListFragment : Fragment() {

    companion object {
        const val TAG = "AudioRecordListFragment"
        fun newInstance() = AudioRecordListFragment()
    }

   //作用域范围为Application的共享ShareViewModel
    private val appShareViewModel: AppShareViewModel by applicationViewModels()
    
    //作用域范围为Activity的共享ShareViewModel
    private val shareViewModel:ShareViewModel by activityViewModels()

    private val audioRecordListViewModel: AudioRecordListViewModel by viewModels()

	private var _binding: AudioRecordListFragmentBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = AudioRecordListFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        appShareViewModel.uiState.collect {
            // New value received
            when (it) {
                AppShareViewModel.UiState.PERMISSION -> {

                }
                AppShareViewModel.UiState.HOME -> {

                }
                AppShareViewModel.UiState.START -> {

                }
                AppShareViewModel.UiState.PAUSE -> {

                }
                AppShareViewModel.UiState.PLAY -> {

                }

            }
        }
 		......
    }

}

Then it's time to have fun.

summary

By imitating the source code of activityViewModels, we define two extension methods: AppCompatActivity.applicationViewModels and Fragment.applicationViewModels, which implement the ViewModel of the Application scope, which is very simple. But is it the right thing to do? Will it cause other problems such as memory leaks? After all, the ViewModel class is designed to store and manage interface-related data in a lifecycle-focused manner that survives configuration changes such as screen rotations. When we do this, it seems to violate this central idea. The author will write another article to analyze this issue in the future, so stay tuned.

Written at the end, first of all, thank you very much for your patience in reading the entire article. It is not easy to insist on writing original and practical articles. If this article happens to be helpful to you, you are welcome to like and comment on the article. Your encouragement is the author's insistence Unrelenting drive. If there are any mistakes in the article, please correct me, thank you again.

Learning is like sailing against the current, if you don’t advance, you will retreat; your heart is like a horse on the plain, easy to let go but hard to chase.

Guess you like

Origin blog.csdn.net/xiangang12202/article/details/122841245