What is the correct way to use viewLifecycleOwner in MainActivity? - kotlin

I am trying to figure out how to correctly use the viewLifecycleOwner in MainActivity, I have read and been told lifecycles are used with fragments. However, I am not implementing fragments in my app. When adding observers in the code, I am using "this" in place of viewLifecycleOwner. This would not rise any errors, but will eventually not work as it doesn't bind data properly in the virtual device (when running the app, it only displays a blank page for the app without data or images). So far, what I have in MainActivity is the following code.
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: DrinkViewModel
// Contains all the views
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
// Use Data Binding to get reference to the views
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.drinkButton.setOnClickListener {
onDrinkClicked()
}
viewModel.revenue.observe(this, Observer { newRevenue ->
binding.revenueText.text = newRevenue.toString()
})
viewModel.drinksSold.observe(this, Observer { newAmount ->
binding.amountSoldText.text = newAmount.toString()
})
}
}

After EpicPandaForce's comment, I focused on whether I was correctly binding the data and images. I realized I was not. I was mistakenly binding revenue and amountSold as texts. I was also trying to set newRevenue and newAmount to strings. Revenue and amountSold were supposed to be passed as Integers. The following code is the correct one.
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: DrinkViewModel
// Contains all the views
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
// Use Data Binding to get reference to the views
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.drinkButton.setOnClickListener {
onDrinkClicked()
}
viewModel.revenue.observe(this, Observer { newRevenue ->
binding.revenue = newRevenue
})
viewModel.drinksSold.observe(this, Observer { newAmount ->
binding.drinkSold = newAmount
})
}
}

Related

Getting list from viewmodel in observe event -MVVM

I have a issue in getting a list returned in observe event in my activity. i am developing a login screen in MVVM. viewmodel is as follows.
my problem is i can get returned data in observe call back into a UI control. but same data returned assign into a list variable is empty. in other words, list returned unable to pass into a a list variable in an activity.
class LoginViewModel #Inject internal constructor (private val loginRepository: LoginRepository,private val usersRepository: UsersRepository): ViewModel() {
private var _userEmail:MutableLiveData<String>
private var _userPassword:MutableLiveData<String>
private var _userLoginData:MutableLiveData<UserLoginData>
private var allUsers:MutableLiveData<List<Users>>
private var findUser:MutableLiveData<List<Users>>
init{
_userEmail= MutableLiveData()
_userPassword= MutableLiveData()
_userLoginData= MutableLiveData()
allUsers= MutableLiveData()
findUser= MutableLiveData()
}
fun getEmail():LiveData<String>{
return _userEmail
}
fun getPassword():MutableLiveData<String>{
return _userPassword
}
fun userLogin(userEmail:String,userPassword:String):MutableLiveData<UserLoginData>{
_userEmail.postValue(userEmail)
_userPassword.postValue(userPassword)
viewModelScope.launch(Dispatchers.IO) {
var userlogindata:UserLoginData=loginRepository.userLogin(userEmail,userPassword)
_userLoginData.postValue(userlogindata)
}
return _userLoginData
}
fun getAllUsers():MutableLiveData<List<Users>>{
//lateinit var _allUsers:List<Users>
viewModelScope.launch(Dispatchers.IO) {
val _allUsers:List<Users> =usersRepository.getUsers()
allUsers.postValue(_allUsers)
}
return allUsers
}
fun findUser(userEmail:String):MutableLiveData<List<Users>>{
//lateinit var finduser:List<Users>
viewModelScope.launch(Dispatchers.IO) {
val _findUser:List<Users> =usersRepository.findUser(userEmail)
findUser.postValue(_findUser)
}
return findUser
}
}
in an activity i am observing the users list and getting the list into a list variable in the activity. code in the activity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var loginViewModel: LoginViewModel
lateinit var loginData:UserLoginData
var users:List<Users> = emptyList()
var findUser:List<Users> = emptyList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
loginViewModel = ViewModelProvider(this).get(LoginViewModel::class.java)
/*observe users list*/
loginViewModel.getAllUsers().observe(this, {It->
users=It
binding.textView.text=It[0].email.toString()
})
loginViewModel.findUser(binding.loginEditTextTextEmailAddressTxt.toString().trim()).observe(this,{it->
findUser=it
})
This program failed if i use data in the users or findUser lists.
Kindly help me to find the best practice in getting the changed data from viewmodel into an activity
ViewModel:
data class User(
var name: String
)
private val _allUsers = MutableLiveData<List<User>>()
private val allUsers: LiveData<List<User>> get() = _allUsers
fun fetchAllUsers(): LiveData<List<User>> {
viewModelScope.launch {
//delay is simulating network request delay
delay(1000)
//listOf is simulating usersRepository.getUsers()
_allUsers.value = listOf(User("name1"), User("name2"), User("name3"))
}
return allUsers
}
Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.fetchAllUsers().observe(viewLifecycleOwner) { userList ->
userList.forEach {
Log.d("user", it.name)
}
}
You can try this way but I do not prefer returning liveData with function because you have to observe liveData once. You need to be careful observe once.

Why is data being shown when screen rotates in jetpack compose

I'm facing this issue where the data I'm retrieving from an API, https://randomuser.me/api/ at first compose it doesn't load.
But every time I rotate the screen the data updates.
First load
After screen rotation
View
class MainActivity : ComponentActivity() {
private val userViewModel : UserViewModel by viewModels()
private var userList: List<UserModel> = listOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userViewModel.userModel.observe(this, Observer {
userList = it
})
userViewModel.onCreate()
setContent {
ListUsers(userList = userList)
}
}
}
ViewModel
class UserViewModel : ViewModel() {
val userModel = MutableLiveData<List<UserModel>>()
var getRandomUsersUseCase = RandomUsersUseCase()
fun onCreate() {
viewModelScope.launch {
val result = getRandomUsersUseCase()
if(!result.isNullOrEmpty()){
userModel.postValue(result)
}
}
}
}
Use State to ensure the data changes trigger recomposition of the Composable.
If you use another observable type such as LiveData in Compose, you
should convert it to State before reading it in a composable using
a composable extension function like LiveData.observeAsState().
Changes to your code would be,
val userListState by userViewModel.userModel.observeAsState()
setContent {
ListUsers(userList = userListState)
}
Why does it shows the data during rotation?
When rotating the screen or during any other configuration changes, the activity will be recreated.
More info on that here - Docs
In most cases, you would not require data to be changed when the screen rotates.
If you want to persist the data even after screen rotation, move the code inside onCreate() in your UserViewModel to the init block, like this.
init {
getData()
}
fun getData() {
viewModelScope.launch {
val result = getRandomUsersUseCase()
if(!result.isNullOrEmpty()){
userModel.postValue(result)
}
}
}
If you need to refresh the data on any other event like button click, swipe to refresh, etc, just call the getData() again on the event handler.
P.S: Check correct imports are added as required.
import androidx.compose.runtime.setValue
import androidx.compose.runtime.getValue

When is a factory method required for a Viewmodel using Android MVVM? [duplicate]

We have been discussing about this but we don't know the reason of creating a viewmodel factory to create a viewmodel instead of instantiate the viewmodel directly. What is the gain of creating a factory that just creates the viewmodel?
I just put a simple example of how I did it without Factory
here is the kodein module:
val heroesRepositoryModel = Kodein {
bind<HeroesRepository>() with singleton {
HeroesRepository()
}
bind<ApiDataSource>() with singleton {
DataModule.create()
}
bind<MainViewModel>() with provider {
MainViewModel()
}
}
The piece of the Activity where I instantiate the viewmodel without using the factory
class MainActivity : AppCompatActivity() {
private lateinit var heroesAdapter: HeroAdapter
private lateinit var viewModel: MainViewModel
private val heroesList = mutableListOf<Heroes.MapHero>()
private var page = 0
private var progressBarUpdated = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this)
.get(MainViewModel::class.java)
initAdapter()
initObserver()
findHeroes()
}
The ViewModel where I instantiate the usecase directly without having it in the constructor
class MainViewModel : ViewModel(), CoroutineScope {
private val heroesRepository: HeroesRepository = heroesRepositoryModel.instance()
val data = MutableLiveData<List<Heroes.MapHero>>()
private var job: Job = Job()
override val coroutineContext: CoroutineContext
get() = uiContext + job
fun getHeroesFromRepository(page: Int) {
launch {
try {
val response = heroesRepository.getHeroes(page).await()
data.value = response.data.results.map { it.convertToMapHero() }
} catch (e: HttpException) {
data.value = null
} catch (e: Throwable) {
data.value = null
}
}
}
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
So here a example using factory
class ListFragment : Fragment(), KodeinAware, ContactsAdapter.OnContactListener {
override val kodein by closestKodein()
private lateinit var adapterContacts: ContactsAdapter
private val mainViewModelFactory: MainViewModelFactory by instance()
private val mainViewModel: MainViewModel by lazy {
activity?.run {
ViewModelProviders.of(this, mainViewModelFactory)
.get(MainViewModel::class.java)
} ?: throw Exception("Invalid Activity")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_list, container, false)
}
The viewmodelfactory:
class MainViewModelFactory (private val getContacts: GetContacts) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(getContacts) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
And the viewmodel:
class MainViewModel(private val getContacts: GetContacts) : BaseViewModel() {
lateinit var gamesList: LiveData<PagedList<Contact>>
var contactsSelectedData: MutableLiveData<List<Contact>> = MutableLiveData()
var contactsSelected: ArrayList<Contact> = ArrayList()
private val pagedListConfig by lazy {
PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(PAGES_CONTACTS_SIZE)
.setPageSize(PAGES_CONTACTS_SIZE)
.setPrefetchDistance(PAGES_CONTACTS_SIZE*2)
.build()
}
Here is the complete first example:
https://github.com/ibanarriolaIT/Marvel/tree/mvvm
And the complete second example:
https://github.com/AdrianMeizoso/Payment-App
We can not create ViewModel on our own. We need ViewModelProviders utility provided by Android to create ViewModels.
But ViewModelProviders can only instantiate ViewModels with no arg constructor.
So if I have a ViewModel with multiple arguments, then I need to use a Factory that I can pass to ViewModelProviders to use when an instance of MyViewModel is required.
For example -
public class MyViewModel extends ViewModel {
private final MyRepo myrepo;
public MyViewModel(MyRepo myrepo) {
this.myrepo = myrepo;
}
}
To instantiate this ViewModel, I need to have a factory which ViewModelProviders can use to create its instance.
ViewModelProviders Utility can not create instance of a ViewModel with argument constructor because it does not know how and what objects to pass in the constructor.
In short,
if we need to pass some input data to the constructor of the viewModel , we need to create a factory class for viewModel.
Like example :-
class MyViewModelFactory constructor(private val repository: DataRepository): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(MyViewModel::class.java!!)) {
MyViewModel(this.repository) as T
} else {
throw IllegalArgumentException("ViewModel Not Found")
}
}
}
Reason
We cannot directly create the object of the ViewModel as it would not be aware of the lifecyclerOwner. So we use :-
ViewModelProviders.of(this, MyViewModelFactory(repository)).get(MyViewModel::class.java)
We have been discussing about this but we don't know the reason of creating a viewmodel factory to create a viewmodel instead of instantiate the viewmodel directly. What is the gain of creating a factory that just creates the viewmodel?
Because Android will only give you a new instance if it's not yet created for that specific given ViewModelStoreOwner.
Let's also not forget that ViewModels are kept alive across configuration changes, so if you rotate the phone, you're not supposed to create a new ViewModel.
If you are going back to a previous Activity and you re-open this Activity, then the previous ViewModel should receive onCleared() and the new Activity should have a new ViewModel.
Unless you're doing that yourself, you should probably just trust the ViewModelProviders.Factory to do its job.
(And you need the factory because you typically don't just have a no-arg constructor, your ViewModel has constructor arguments, and the ViewModelProvider must know how to fill out the constructor arguments when you're using a non-default constructor).
When we are simply using ViewModel, we cannot pass arguments to that ViewModel
class GameViewModel() : ViewModel() {
init {
Log.d(TAG, "GameViewModel created")
}
}
However, in some cases, we need to pass our own arguments to ViewModel. This can be done using ViewModelFactory.
class ScoreViewModel(finalScore: Int) : ViewModel() {
val score = finalScore
init {
Log.d(TAG, "Final score: $finalScore")
}
}
And to instantiate this ViewModel, we need a ViewModelProvider.Factory as simple ViewModel cannot instantiate it.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
return ScoreViewModel(finalScore) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
When it comes to instantiating object of this ViewModel i.e with ViewModelProvider, we pass ViewModelFactory as an argument which contains information about our custom arguments which we want to pass. It goes like:
viewModelFactory = ScoreViewModelFactory(score)
viewModel = ViewModelProvider(this,viewModelFactory).get(ScoreViewModel::class.java)
That is why factory methods are there.

how Live data will be update in MVVM

I want to get input from the user using EditText and pass it to server and show the response to the user. I do this simply without any architecture but I would like to implement it in MVVM.
this is my repository code:
class Repository {
fun getData(context: Context, word: String): LiveData<String> {
val result = MutableLiveData<String>()
val request = object : StringRequest(
Method.POST,
"https://jsonplaceholder.typicode.com/posts",
Response.Listener {
result.value = it.toString()
},
Response.ErrorListener {
result.value = it.toString()
})
{
#Throws(AuthFailureError::class)
override fun getParams(): MutableMap<String, String> {
val params = HashMap<String, String>()
params["word"] = word
return params
}
}
val queue = Volley.newRequestQueue(context)
queue.add(request)
return result
}
}
and these are my View Model codes:
class ViewModel(application: Application) : AndroidViewModel(application) {
fun getData(word: String): LiveData<String> {
val repository = Repository()
return repository.getData(getApplication(), word)
}
}
and my mainActivity would be like this:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val model = ViewModelProviders.of(this).get(ViewModel::class.java)
model.getData("test").observe(this, Observer {
Log.i("Log", "activity $it")
})
}
}
My layout has an EditText which I want to get user input and pass it to the server, how should i do that?
Here how i did it in my projet.
You can probably use android annotations.
It's gonna requiere you to put some dependencies and maybe change the class a bit, but then you gonna link your Viewmodel with your repository and then you gonna have to program the setter of the variable to do a notifyChange() by making the class herited from BaseObservable. Then in the xml, if you did the correctly, you should be able to do a thing like text:"#={model.variable}" and it should be updating at the same time.
A bit hard and explain or to show for me sorry, but i would look into Android Annotations with #DataBinding, #DataBound :BaseObservable
https://github.com/androidannotations/androidannotations/wiki/Data-binding-support
Hope that can help a bit!

Issue overriding onDrawerOpened() - Kotlin

I'm working on a Kotlin coded app using the Navigation Drawer Layout with a list of items. When I open the drawer using the icon at the top left I want it to notifyDataSetChanged so that the list is updated from outside the MainActivity. This includes pressing the back button, swiping open the drawer, or clicking the stacked lines icon at the top left
Here's a simplified version of my code, but it's essentially a Navagation Drawer Activity with a recyclerView for the context of my question:
class MainActivity : AppCompatActivity() {
lateinit var drawerLayout: DrawerLayout
lateinit var navView: NavigationView
lateinit var toolbar: Toolbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
toolbar = findViewById(R.id.app_bar_toolbar)
setSupportActionBar(toolbar)
drawerLayout = findViewById(R.id.drawer_layout)
navView = nav_view
val toggle = ActionBarDrawerToggle(this, drawerLayout, toolbar,
R.string.navigation_drawer_open, R.string.navigation_drawer_close)
drawerLayout.addDrawerListener(toggle)
toggle.syncState()
}
I've spent several hours trying anything I can, but primarily I can't get this to work:
override fun onDrawerOpen(view:View){
... myAdapter.notifyDataSetChanged()
super.onDrawerOpen(view)
}
it gives me the hint "overrides nothing" or "unused", and if I try to add it to any of my code in onCreate it states "Modifier 'override' not applicable to local function.
What am I doing wrong? Is there a better way to notify a data change when the drawer is opened?
The Question is old, but to whom has the same problem:
Just Create new class that implements DrawerListener and write your logic inside appropriate method:
private inner class MyDrawerListener(): DrawerListener {
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
}
override fun onDrawerOpened(drawerView: View) {
}
override fun onDrawerClosed(drawerView: View) {
}
override fun onDrawerStateChanged(newState: Int) {
}
}
Now add listener as:
drawerLayout.addDrawerListener(MyDrawerListener())