In my app I have two activities. The main activity that only has a search button in the Appbar and a second, searchable, activity. The second activity hold a fragment that fetches the data searched in it's onCreate call. My problem is that the fragment fetches the data twice. Inspecting the lifecycle of my activities, I concluded that the searchable activity gets paused at some point, which obviously determines the fragment to be recreated. But I have no idea what causes the activity to be paused.
Here are my activities
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val root = binding.root
setContentView(root)
//Setup the app bar
setSupportActionBar(binding.toolbar);
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return initOptionMenu(menu, this)
}
}
fun initOptionMenu(menu: Menu?, context: AppCompatActivity): Boolean {
val inflater = context.menuInflater;
inflater.inflate(R.menu.app_bar_menu, menu)
// Get the SearchView and set the searchable configuration
val searchManager = context.getSystemService(Context.SEARCH_SERVICE) as SearchManager
(menu?.findItem(R.id.app_bar_search)?.actionView as SearchView).apply {
// Assumes current activity is the searchable activity
setSearchableInfo(searchManager.getSearchableInfo(context.componentName))
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
}
return true;
}
SearchActivity.kt
class SearchActivity : AppCompatActivity() {
private lateinit var viewBinding: SearchActivityBinding
private var query: String? = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = SearchActivityBinding.inflate(layoutInflater)
val root = viewBinding.root
setContentView(root)
// Setup app bar
supportActionBar?.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
supportActionBar?.setCustomView(R.layout.search_app_bar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
//Get the query string
if (Intent.ACTION_SEARCH == intent.action) {
intent.getStringExtra(SearchManager.QUERY).also {
//Add the query to the appbar
query = it
updateAppBarQuery(it)
}
}
//Instantiate the fragment
if (savedInstanceState == null) {
val fragment = SearchFragment.newInstance();
val bundle = Bundle();
bundle.putString(Intent.ACTION_SEARCH, query)
fragment.arguments = bundle;
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment)
.commitNow()
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return initOptionMenu(menu, this)
}
private fun updateAppBarQuery(q: String?) {
supportActionBar?.customView?.findViewById<TextView>(R.id.query)?.apply {
text = q
}
}
}
As you can see, I am using the built in SearchManger to handle my search action and switching between activities. I haven't seen anywhere in the docs that during search, my searchable activity might get paused or anything like that. Does anyone have any idea why this happens? Thanks in advance!
edit: Here is my onCreate method for the SearchFragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val query = arguments?.getString(Intent.ACTION_SEARCH);
//Create observers
val searchResultObserver = Observer<Array<GoodreadsBook>> {
searchResultListViewAdapter.setData(it)
}
viewModel.getSearchResults().observe(this, searchResultObserver)
GlobalScope.launch { //Perform the search
viewModel.search(query)
}
lifecycle.addObserver(SearchFragmentLifecycleObserver())
}
Here, searchResultListViewAdapter is the adapter for a RecyclerViewand searchResult is a livedata in the view-model holding the search result
Here is the stack trace for the first call of onCreate() on SearchFragment:
And here is for the second call:
Here is the ViewModel for the SearchFragment:
class SearchViewModel() : ViewModel() {
private val searchResults: MutableLiveData<Array<GoodreadsBook>> by lazy {
MutableLiveData<Array<GoodreadsBook>>();
}
fun getSearchResults(): LiveData<Array<GoodreadsBook>> {
return searchResults;
}
// TODO: Add pagination
suspend fun search(query: String?) = withContext(Dispatchers.Default) {
val callback: Callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
// TODO: Display error message
}
override fun onResponse(call: Call, response: Response) {
// TODO: Check res status
val gson = Gson();
val parsedRes = gson.fromJson(
response.body?.charStream(),
Array<GoodreadsBook>::class.java
);
// Create the bitmap from the imageUrl
searchResults.postValue(parsedRes)
}
}
launch { searchBook(query, callback) }
}
}
I made some changes to the app since posted this and right now the search doesn't work for some reason in the main branch. This ViewModel it's from a branch closer to the time I posted this. Here is the current ViewModel, although the problem is present in this variant as well:
class SearchViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
// private val searchResults: MutableLiveData<Array<GoodreadsBook>> by lazy {
//// MutableLiveData<Array<GoodreadsBook>>();
//// }
companion object {
private const val SEARCH_RESULTS = "searchResults"
}
fun getSearchResults(): LiveData<Array<GoodreadsBook>> =
savedStateHandle.getLiveData<Array<GoodreadsBook>>(SEARCH_RESULTS)
// TODO: Add pagination
fun search(query: String?) {
val searchResults = savedStateHandle.getLiveData<Array<GoodreadsBook>>(SEARCH_RESULTS)
if (searchResults.value == null)
viewModelScope.launch {
withContext(Dispatchers.Default) {
//Handle the API response
val callback: Callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
// TODO: Display error message
}
override fun onResponse(call: Call, response: Response) {
// TODO: Check res status
val gson = Gson();
val parsedRes = gson.fromJson(
response.body?.charStream(),
Array<GoodreadsBook>::class.java
);
searchResults.postValue(parsedRes)
}
}
launch { searchBook(query, callback) }
}
}
}
}
The searchBook function just performs the HTTP request to the API, all the data manipulation is handled in the viewModel
try this way
Fragment sf = SearchFragment.newInstance();
Bundle args = new Bundle();
args.putString(Intent.ACTION_SEARCH, query);
sf.setArguments(args);
getFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, sf).addToBackStack(null).commit();
If your activity is getting paused in between then also onCreate of your activity should not be called and that's where you are instantiating the fragment.i.e Fragment is not created again(view might be created again).
As as you have subscribed live data in onCreate of Fragment it should also not trigger an update(onChanged() won't be called for liveData) again.
Just to be sure about live data is not calling onChanged() again try below (i feel that's the culprit here as i can't see any other update happening)
As you will not want to send the same result to your search page again so distinctUntilChanged is a good check for your case.
viewModel.getSearchResults().distinctUntilChanged().observe(viewLifecycleOwner,
searchResultObserver)
Do subscription of live data in onActivityCreated of
fragment.(reference)
Instead of using globalScope you can use viewModelScope and launch from inside your ViewModel.(just a suggestion for clean code)
And what's SearchFragmentLifecycleObserver?
P.S - If you can share the ViewModel code and how the search callback's are triggering data it will be great.But Current lifecycle should not effect the creation of new fragment.
Use SaveStateHandle in your ViewModel to persist the loaded data, and don't use GlobalContext to do the fetching, encapsulate the fetching in VieModel. GlobalContext should only be used for fire and forget actions, which are not bound the any views or lifecycle.
How your SearchViewModel could look like:
#Parcelize
class SearchResult(
//fields ...
) : Parcelable
class SearchViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
private var isLoading : Boolean = false
fun searchLiveData() : LiveData<SearchResult> = savedStateHandle.getLiveData<SearchResult>(EXTRA_SEARCH)
fun fetchSearchResultIfNotLoaded() { //do this in onCreate
val liveData = savedStateHandle.getLiveData<SearchResult>(EXTRA_SEARCH)
if(liveData.value == null) {
if(isLoading)
return
isLoading = true
viewModelScope.launch {
try {
val result = withContext(Dispatchers.IO) {
//fetching task
SearchResult()
}
liveData.value = result
isLoading = false
}catch (e : Exception) {
//log
isLoading = false
}
}
}
}
companion object {
private const val EXTRA_SEARCH = "EXTRA_SEARCH"
}
}
And in your Search Fragment onCreate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val searchResultObserver = Observer<Array<GoodreadsBook>> {
searchResultListViewAdapter.setData(it)
}
viewModel.searchLiveData().observe(viewLifeCycleScope, searchResultObserver)
viewModel.fetchSearchResultIfNotLoaded()
}
I think the Android team in charge of the documentation should really do a better job. I went ahead and just removed the SearchManager from the SearchViewand use the onQueryTextListener directly, only to see that with this approach I also get my listener called twice. But thanks to this post, I saw that apparently it's a bug with the emulator (or with the way SearchView handles the submit event). So if I press the OSK enter button everything works as expected.
Thanks everyone for their help!
Related
I've been reading some questions, answers and blogs about MVVM pattern in Android, and I've implemented it in my application.
My application consists of a MainActivity with 3 Tabs. Content of each tab is a fragment.
One of these fragments, is a List of Users stored on Room DB, which is where I've implemented the MVVM (implementing User object, ViewModel, Repository and Adapter with RecycleView).
In this same fragment, I have an "add User" button at the end that leads to a new activity where a formulary is presented to add a new user. In this activity I want to be sure that the full name of user not exists in my DB before saving it.
I was trying to use the same ViewModel to get full UserNames full name, but it seems that ViewModel is never initialized and I dont' know why.
I've read some questions about that viewmodel can't be used in different activities (I use it in MainActivity also in AddUser activity
This is my ViewModel:
class UserViewModel : ViewModel() {
val allUsersLiveData: LiveData<List<User>>
private val repository: UserRepository
init {
Timber.i("Initializing UserViewModel")
repository = UserRepository(UserTrackerApplication.database!!.databaseDao())
allUsersLiveData = repository.getAllUsers()
}
fun getAllUsersFullName(): List<String> {
return allUsersLiveData.value!!.map { it.fullname}
}
And my AddUser activity:
class AddUser : AppCompatActivity() {
private lateinit var userList:List<String>
private lateinit var binding: ActivityAddUserBinding
private val userViewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_user)
Timber.i("Add User OnCreate")
binding = ActivityAddUserBinding.inflate(layoutInflater)
setContentView(binding.root)
}
fun addUserClick(v : View){
//someCode
val userName = binding.constraintLayoutAddUser.etUserName!!.text.toString()
if(checkUserExistance(userName)) {
val text: String = String.format(
resources.getString(R.string.repeated_user_name),
userName
Snackbar.make(v, text, Snackbar.LENGTH_LONG).show()
{
else
{
lifecycleScope.launch {
UserTrackerApplication.database!!.databaseDao()
.insertUser(user)
Timber.i("User added!")
}
finish()
}
}
Debugging, I see the log "Initializing UserViewModel" when the fragment of MainActivity is started, but I can't see it when AddUser activity is called. So it seems it's not initializing correctly.
So the questions:
Is this a good approach? I'm making some design mistake?
Why the VM isn't initializing?
EDIT
I forgot to add this function. Calling userViewModel here is where I get the error:
private fun checkUserExistance(userName: String): Boolean {
var result = false
userList = userViewModel.getAllUsersNames()
for (usr in userList)
{
if(usr.uppercase() == userName.uppercase())
{
result = true
break
}
}
return result
}
EDIT 2
I added this on my "onCreate" function and started to work:
userViewModel.allUsersLiveData.observe(this, Observer<List<User>>{
it?.let {
// updates the list.
Timber.i("Updating User Names")
userList =userViewModel.getAllUsersNames()
}
})
if you take a look at by viewModels delegate you will see it's lazy it means it will initialize when it is first time accessed
#MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}
I think my observable is set incorrectly here. I am using Retrofit2 + Moshi as the deserializer, and the API call from Retrofit is working.
But once I make the API call, I am trying to set up the Observable in my Activity and then use the API call data from the data class.
Here is my view model code:
class DealsViewModel(val repository: MainRepository) : ViewModel() {
val movieList = MutableLiveData<List<DealItems>>()
var job: Job? = null
val loading = MutableLiveData<Boolean>()
val errorMessage = MutableLiveData<String>()
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
fun getMovies() {
viewModelScope.launch{
// View Model Scope gives the Coroutine that will be canceled when the ViewModel is cleared.
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val items = repository.getProduct()
withContext(Dispatchers.Main) {
if (items.isNullOrEmpty()) {
loading.value = false
// put error message in here later
} else {
dealList.postValue(items)
return#withContext
}
}
}
}
}
private fun onError(message: String) {
errorMessage.value = message
loading.value = false
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
}
And here is my MainActivity code.
I am using JetpackCompose in my activity, LiveData for the API response container. In my main repository is where I am validating a successful API response and then the coroutines for the call are inside of the view model.
My API call is successful, but I am not sure where to call the ViewModel.GetMovies() inside of the activity and I am not sure if the observables are set properly and/or where to pass the API's livedata into my composable function.
Thanks for any help you can provide. I am new to android and trying to use Coroutines for the first time.
class MainActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val retrofitService = RetrofitService.getInstance()
val viewModel = ViewModelProvider(this,
MyViewModelFactory(MainRepository(retrofitService = retrofitService))).get(DealsViewModel::class.java)
// viewModel.getProducts()
setContent {
myApp {
MyScreenContent()
}
viewModel.movieList.observe(
this, { it ->
if( it != null) {
it.forEach {
var movieLocation = it.movieLocation
val description = it.description
val id = it.id
val title = it.title
val regularPrice = it.regularPrice
}
}
})
return#setContent
}
viewModel.errorMessage.observe(this, {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
})
viewModel.loading.observe(
this,
Observer {
if (it) {
}
})
}
}
I assume that it always depends when should you call especially in the activity we have many lifecycles; however, the best way is to use the .also on the livedata/stateflow lazy creation so that you do guarantee as long as the view model is alive, the getMovies is called only one time, and also guarantee the service itself is not called unless someone is listening to it.
You may check the full documentation in this link
Here is a code example
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers()
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
When using this code, you do not have to call getMovies at all in the activity, you just listen to the observer.
I want to load data from an API when activity is started. Currently, I call a view model's method from the activity to load data and it's working fine, but I don't know if it's the best way to do it:
Activity
override fun onCreate(savedInstanceState: Bundle?) {
//initialize stuff...
viewModel.myData.observe(this) {
//do things with the data
}
lifeCycleScope.launch { viewModel.loadData() }
}
ViewModel
class MyViewModel : ViewModel() {
val myData = MutableLiveData<MyData>()
suspend fun loadData() = withContext(Dispatchers.IO) {
val data = api.getData()
withContext(Dispatchers.Main) {
myData.value = data
}
}
}
I have seen some examples using lazy initialization, but I don't know how to implement it with coroutines. I have tried this:
Activity
override fun onCreate(savedInstanceState: Bundle?) {
//initialize stuff...
viewModel.myData().observe(this) {
//do things with the data
}
}
ViewModel
private val myData : MutableLiveData<MyData> by lazy {
MutableLiveData<MyData>().also {
viewModelScope.launch {
loadData()
}
}
}
fun myData() = myData
suspend fun loadData() = // same as above
But data is not fetched and nothing is displayed.
If you've added dependency livedata-ktx then you can use livedata builder to also have API call in same block and emit. Checkout how you can do it:
class MyViewModel : ViewModel() {
val myData: LiveData<MyData> = liveData {
val data = api.getData() // suspended call
emit(data) // emit data once available
}
}
I can't observe the LiveData<PagedList> change in activity, but the UI is updated(the list has grown in activty).
I can only observe it once when the livedata is initialized.
when the paging library call loadAfter method, the ui is updated, but didn't call pageList.observe{}
Firstly, I put the process of data request into the Kotlin Coroutines, I can't observe the data change, then I used asynchronous requests instead.It still didn't work.
Here is my code:
PlayActivity main code
private val commentAdapter =
object : BasePagedAdapter(diffCallback, this) {
// just bind recycleview item and corresponding view model. etc.
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_play)
binding.vm = vm
binding.lifecycleOwner = this
val workId = intent.getLongExtra(WORK_ID, 0)
vm.listComment(workId)
play_rv_comment.adapter = commentAdapter
/* herer is the problem*/
vm.commentList.observe(this, Observer {
/*only log once when called loadInitial*/
LogUtils.e("observe", it)
commentAdapter.submitList(it)
})
PlayViewModel
class PlayViewModel : BaseViewModel() {
var workId: Long = 0
// the data which I want to observe
lateinit var commentList: LiveData<PagedList<WorkComment>>
private val commentPageSize = 15
fun listComment(workId: Long) {
// init by DataSource.Factory in android paging library
commentList = BaseDataSourceFactory(workId).toLiveData(commentPageSize)
}
DataSource.Factory in Android paging
class BaseDataSourceFactory(
val workId: Long
) :
DataSource.Factory<Long, WorkComment>() {
override fun create(): DataSource<Long, WorkComment> {
return object : PageKeyedDataSource<Long, WorkComment>() {
override fun loadInitial(
params: LoadInitialParams<Long>,
callback: LoadInitialCallback<Long, WorkComment>
) {
try {
val res = RetrofitUtil.getInstanceWithJwt().create(WorkCommentApi::class.java)
.listComment(
workId, 1, params.requestedLoadSize
)
res.enqueue(object : retrofit2.Callback<TResult> {
override fun onFailure(call: Call<TResult>, t: Throwable) {
}
override fun onResponse(call: Call<TResult>, response: Response<TResult>) {
callback.onResult(
response.body()!!.toList(WorkComment::class.java),
null, 2)
}
})
} catch (e: SocketTimeoutException) {
ToastUtils.showShort("请稍候重试")
} catch (e: Exception) {
LogUtils.e(e.localizedMessage)
}
}
// called many times, but I can't observe the PagedList change
override fun loadAfter(
params: LoadParams<Long>,
callback: LoadCallback<Long, WorkComment>
) {
val res = RetrofitUtil.getInstanceWithJwt().create(WorkCommentApi::class.java)
.listComment(
workId, 1, params.requestedLoadSize
)
res.enqueue(object : retrofit2.Callback<TResult> {
override fun onFailure(call: Call<TResult>, t: Throwable) {
}
override fun onResponse(call: Call<TResult>, response: Response<TResult>) {
callback.onResult(
response.body()!!.toList(WorkComment::class.java),
params.key + 1
)
}
})
}
override fun loadBefore(
params: LoadParams<Long>,
callback: LoadCallback<Long, WorkComment>
) {
}
}
}
}
Retrofit Api
interface WorkCommentApi {
/**
* list comment
*/
#GET("public/work/comment")
fun listComment(#Query("workId") workId: Long, #Query("current") current: Long, #Query("size") size: Int): Call<TResult>
}
I want to know what should I do to observe the LiveData<PagedList> change
This is happening because each time you call vm.listComment(workId), the object you first bound in activity is killed and new object is created.
You can use Transformations with MediatorLiveData.
Activity:
viewModel.logout().observe(this, Observer {
// do here
})
ViewModel:
class RepackViewModel(app: Application) : BaseViewModel(app) {
// IMPORTANT - Mediator
val logout = MediatorLiveData<PagedList<WorkComment>>()
fun logout() : LiveData<PagedList<WorkComment>> = logout
init {
// IMPORTANT - passes repo update to activity
logout.addSource(repo.getLogoutResponse()) { logout.postValue(it) }
}
}
Repository:
class BaseRepository(val app: Application) {
private val logout = MutableLiveData<PagedList<WorkComment>>()
fun getLogoutResponse(): LiveData<PagedList<WorkComment>> = logout
override fun create(): DataSource<Long, WorkComment> {
//when you get your data
logout.value = // your value
}
You need to have your work id be mutable data to be observed by the transformation. so whenever you update your work, id, it will fetch comments. Like Thus...
ViewModel:
val workIdMutableLiveData: MutableLiveData<Int> = MutableLiveData(workId)
//This performs the meat of the work to display the items in the recyclerview
var commentsList = Transformations.switchMap(workIdMutableLiveData) { workId ->
val config = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(pagingLimit)
.build()
val pagedListBuilder = LivePagedListBuilder<Long, WorkComment>(BaseDataSourceFactory(workId), config)
pagedListBuilder.build()
}
Then in your activity, observe
yourViewModel.commentsList.observe(this, Observer { list ->
list ?: return#Observer
adapter.submitList(list)
yourRecyclerView.adapter = adapter
})
Whenever you update the workIdMutableLiveData by doing a
workIdMutableLiveData.postValue(workId)
...The recyclerview will update. Your recyclerview must inherit from PagedListAdapter.
After testing, I knew list couldn't be observed when it has inner data change, like add(), remove(). etc.
It only be observed when its reference has been changed, like create or assignment operation:
list.value = null
So I couldn't observe the data change of LiveData<List>
I have an Activity that has a fragment with a viewmodel in it. The Activity needs to be able to update the livedata object's value as well as does the fragment.
I declared my ViewModel for the fragment like this:
class BottomNavViewModel:ViewModel() {
var isConnected = MutableLiveData<Boolean>()
}
In the BottomNavFragment I have this code to declare the ViewModel
private val viewModel: BottomNavViewModel by lazy { ViewModelProviders.of(this).get(BottomNavViewModel::class.java) }
A few lines below that I have this:
private val changeObserver = Observer<Boolean> { value ->
value?.let {
Timber.i("Update of isConnected received. Updating text field now")
if(it) {
connectedText.text = getString(R.string.connected)
connectedText.setTextColor(activity!!.getColor(R.color.colorSelectedGreen))
}
else {
connectedText.text = getString(R.string.not_connected)
connectedText.setTextColor(activity!!.getColor(R.color.off_red))
}
}
...
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is BottomNavFragment.OnFragmentInteractionListener) {
listener = context
}
else {
throw RuntimeException(context.toString() + " must implement OnFragmentInteractionListener")
}
viewModel.isConnected.observe(this, changeObserver)
}
That Observer never, ever, gets hit.
In my Activity I have this:
private var sharedBottomNavViewModel:BottomNavViewModel? = null
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_connection)
sharedBottomNavViewModel = ViewModelProviders.of(this).get(BottomNavViewModel::class.java)
...
override fun onResume() {
super.onResume()
startBackgroundThread()
checkCameraPermission()
//TODO: Change this to listen for a connection
sharedBottomNavViewModel?.let {
Timber.i("Updating isConnected to true now")
it.isConnected.value = true
}
}
In the logs I see the messages indicating that the update occurs but the observer never gets the message.
Can anyone tell me what I am doing wrong here please?
Your 2 viewmodels are not the same. You are creating a viewmodel and passing the lifecycle owner, and in one case you specify the fragment, and in the other the activity.
Change your fragment like this:
private val viewModel: BottomNavViewModel by lazy { ViewModelProviders.of(activity).get(BottomNavViewModel::class.java) }
Be careful where you initialize the viewmodel though, as activity (getActivity()) is nullable.
Edit: (credit Ian Lake) Or, if you use the fragment-ktx artifacts, you can do this
private val viewModel: BottomNavViewModel by activityViewModels()