How can I initialize in onResponse function? (Android Kotlin) - android

I thought when response.body()?.let {} run, enrollBookAdapter (or booklist = it.books as mutableList<book>) would be initialized but, it did not... What am I doing wrong? How can I initailize bookList with Books...
class EnrollBookActivity : AppCompatActivity() {
private lateinit var binding: ActivityEnrollBookBinding
private lateinit var bookService: BookService
private lateinit var bookList:MutableList<Book>
private lateinit var enrollBookAdapter: EnrollBookAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEnrollBookBinding.inflate(layoutInflater)
setContentView(binding.root)
initBookService()
bookServiceLoadBestSellers()
binding.selfBtn.setOnClickListener{
startActivity(Intent(this,SelfWriteActivity::class.java))
}
enrollBookAdapter.setOnBookClickListener(object:EnrollBookAdapter.OnBookClickListener{
override fun onBookClick(position: Int, book: Book) {
val intent = Intent(this#EnrollBookActivity,TalerMain::class.java)
intent.putExtra("title",enrollBookAdapter.bookList[position].title)
intent.putExtra("url",enrollBookAdapter.bookList[position].title)
startActivity(intent)
}
})
}
private fun initBookRecyclerView(bookList: MutableList<Book>) {
enrollBookAdapter = EnrollBookAdapter(bookList)
enrollBookAdapter.setOnBookClickListener(object:EnrollBookAdapter.OnBookClickListener{
override fun onBookClick(position: Int, book: Book) {
val intent = Intent(this#EnrollBookActivity,TalerMain::class.java)
intent.putExtra("title",enrollBookAdapter.bookList[position].title)
intent.putExtra("url",enrollBookAdapter.bookList[position].title)
startActivity(intent)
}
})
binding.bookRv.layoutManager = GridLayoutManager(this,2)
binding.bookRv.adapter = EnrollBookAdapter(bookList)
binding.bookRv.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
}
private fun bookServiceLoadBestSellers() {
bookService.getBestSellerBooks(getString(R.string.interparkAPIKey))
.enqueue(object : Callback<BestSellerDto> {
override fun onResponse(
call: Call<BestSellerDto>,
response: Response<BestSellerDto>
) {
if (response.isSuccessful.not()) {
Log.e(M_TAG, "NOT!! SUCCESS")
return
}
response.body()?.let {
Log.d(M_TAG, it.toString())
it.books.forEach { book ->
Log.d(M_TAG, book.toString())
}
//enrollBookAdapter = EnrollBookAdapter(it.books as MutableList<Book>)
initBookRecyclerView(it.books as MutableList<Book>)
//setRecyclerView(it.books)
}
}
override fun onFailure(call: Call<BestSellerDto>, t: Throwable) {
Log.e(M_TAG, t.toString())
}
})
}
}
Error
"java.lang.RuntimeException: Unable to start activity ComponentInfo{com.clone.practice/com.clone.practice.EnrollBookActivity}: kotlin.UninitializedPropertyAccessException: lateinit property enrollBookAdapter has not been initialized"
I thought when response.body()?.let {} run, enrollBookAdapter (or booklist = it.books as mutableList<book>) would be initialized but, it did not... What am I doing wrong? How can I initialize bookList with Books...

Try to add initBookRecyclerView before enrollBookAdapter.setOnBookClickListener(object:EnrollBookAdapter.OnBookClickListener line in onCreate.
After that uncomment enrollBookAdapter = EnrollBookAdapter(it.books as MutableList<Book>) and comment the next line.

just remove this code from your onCreate method only as you are trying to use enrollBookAdapter variable before initialisation which is causing crash .
And you have already set this method in initBookRecyclerView method so no need in onCreate method
enrollBookAdapter.setOnBookClickListener(object:EnrollBookAdapter.OnBookClickListener{
override fun onBookClick(position: Int, book: Book) {
val intent = Intent(this#EnrollBookActivity,TalerMain::class.java)
intent.putExtra("title",enrollBookAdapter.bookList[position].title)
intent.putExtra("url",enrollBookAdapter.bookList[position].title)
startActivity(intent)
}
})

Related

Configuration Changes Not Save App State MVVM

I am building a movie app. There is a recyclerview matching parent size,and 1 search action button(SearchView).
When I search for a movie,everything is working fine,but when I change orientation,the activity just lose it's state. the recyclerview turns empty and I need to search for the movie again.
I am using MVVM and I know its not suppose to happen..
Thank you!
This is the Repository:
class MainRepository {
private val searchAfterMutableLiveData = MutableLiveData<List<Movie>>()
private val apiService : GetFromApi = APIService.retrofitClientRequest
private val apiKey = "censored"
fun searchAfter(searchAfter : String) : MutableLiveData<List<Movie>>{
apiService.searchAfter(apiKey,searchAfter)?.enqueue(object : Callback<MovieListResult?> {
override fun onResponse(
call: Call<MovieListResult?>,
response: Response<MovieListResult?>
) {
if (response.isSuccessful){
searchAfterMutableLiveData.value = response.body()?.moviesResults
Log.e("SearchMovieListResults","Result: ${searchAfterMutableLiveData.value}")
}
}
override fun onFailure(call: Call<MovieListResult?>, t: Throwable) {
Log.e("SearchMovieListResults","Failed: ${t.message}")
}
})
return searchAfterMutableLiveData
}
}
This is the ViewModel:
class MainViewModel : ViewModel(){
fun getMovieBySearch(searchAfter : String) : LiveData<List<Movie>>{
return mainRepository.searchAfter(searchAfter)
}
}
This is the MainActivity:
class MainActivity : AppCompatActivity() {
private val mainViewModel : MainViewModel by viewModels()
private lateinit var mainRecyclerView : RecyclerView
private lateinit var mainAdapter : MainRecyclerViewAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initRecyclerView()
}
private fun initRecyclerView() {
mainRecyclerView = findViewById(R.id.mainRecyclerView)
mainRecyclerView.setHasFixedSize(true)
mainRecyclerView.layoutManager = GridLayoutManager(this,1)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu,menu)
val searchView = menu.findItem(R.id.menu_search_movie).actionView as androidx.appcompat.widget.SearchView
searchView.queryHint = "Search By Name,Actor .."
searchView.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(whileTextChange: String?): Boolean {
//Clear SearchView
searchView.isIconified = true
searchView.setQuery("", false)
searchView.onActionViewCollapsed()
mainViewModel.getMovieBySearch(whileTextChange.toString()).observe(this#MainActivity,object : Observer<List<Movie>?> {
override fun onChanged(newList: List<Movie>?) {
if (newList != null) {
mainAdapter = MainRecyclerViewAdapter(newList)
mainRecyclerView.adapter = mainAdapter
//mainAdapter.changeCurrentList(newList)
}
}
})
return false
}
override fun onQueryTextChange(whileTextChange: String?): Boolean {
Log.e("onQueryTextChange","Text: $whileTextChange")
return false
}
})
return true
}
}
You need to save the desired state in the viewmodel. For example,
var persistedMovies = arrayListOf<Movie>()
and when the search returns a valid response,
mainViewModel.persistedMovies = newList
Now the list is scoped to the viewmodel and persists through orientation changes.

where I am making mistake in mainactivity?

I am developing android app and I am getting error screenshot below when I have implemented network call in mainactivity.kt I want to know where I am making mistake
below my MainActivity.kt
class MainActivity : AppCompatActivity() {
private var adapter: CurrenciesAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recycler_main.layoutManager = LinearLayoutManager(this#MainActivity)
adapter = CurrenciesAdapter(this)
recycler_main.adapter = adapter
if (isInternetAvailable()) {
getUsersData()
}
}
private fun getUsersData() {
showProgressBar()
var apiInterface: CurrenciesResponse = CurrencyClient().getApiClient()!!.create(
CurrenciesResponse::class.java
)
apiInterface.getCurrencies().enqueue(object : Callback <List<CurrencyResponse>> {
override fun onResponse(
call: Call<List<CurrencyResponse>>,
response: Response<List<CurrencyResponse>>)
{
hideProgressBar()
val currencyResponse = response.body()
adapter?.list = currencyResponse!!
}
override fun onFailure(call: Call<List<CurrencyResponse>>, t: Throwable) {
hideProgressBar()
Log.e("error", t.localizedMessage)
}
})
}
}
what I have done I have changed to response type from <List to CurrencyResponse but I am still getting response below whole gist code
https://gist.github.com/kyodgorbek/d0d9b3749ac64f15b4db87874cfe13e7
Your getCurrencies() method in CurrenciesResponse.class has a return type of CurrenciesResponse whereas it should be List<CurrenciesResponse>.
You need to fix your retrofit's service interface.

onResume does not worked in viewmodel

my data is fetched only when it is created...im using viewmodel...when press back button it doesnt update the previous data..onresume is not working in this...
i refered this but none of those helped--> Reacting to activity lifecycle in ViewModel
i need help
thanks in advance
activity:--
class MyAccount : BaseClassActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.myaccount)
var mActionBarToolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbartable);
setSupportActionBar(mActionBarToolbar);
setEnabledTitle()
val resetbutton=findViewById<Button>(R.id.resetpwd)
resetbutton.setOnClickListener {
val i=Intent(applicationContext,
ResetPasswordActivity::class.java)
startActivity(i)
}
val editbutton=findViewById<Button>(R.id.editdetail)
editbutton.setOnClickListener {
val i=Intent(applicationContext, EditProfile::class.java)
startActivity(i)
}
hello()
}
override fun onResume() {
super.onResume()
hello()
}
fun hello(){
val first_name = findViewById<TextView>(R.id.firstname)
val last_name = findViewById<TextView>(R.id.lastname)
val emailuser = findViewById<TextView>(R.id.emailuser)
val phone_no = findViewById<TextView>(R.id.phone_no)
val birthday = findViewById<TextView>(R.id.birthday)
val image=findViewById<ImageView>(R.id.imageprofile)
val model = ViewModelProvider(this)[MyAccountViewModel::class.java]
model.viewmodel?.observe(this, object : Observer<My_account_base_response> {
override fun onChanged(t: My_account_base_response?) {
first_name.setText(t?.data?.user_data?.first_name)
last_name.setText(t?.data?.user_data?.last_name)
emailuser.setText(t?.data?.user_data?.email)
phone_no.setText(t?.data?.user_data?.phone_no).toString()
birthday.setText(t?.data?.user_data?.dob).toString()
Glide.with(applicationContext).load(t?.data?.user_data?.profile_pic)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_launcher_foreground)
.into(image)
}
})
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpFromSameTask(this)
true
}
else -> super.onOptionsItemSelected(item)
}
}}
viewmodel:--
class MyAccountViewModel(context: Application) :AndroidViewModel(context),LifecycleObserver{
private var MyAccountViewModels: MutableLiveData<My_account_base_response>? = null
val viewmodel: MutableLiveData<My_account_base_response>?
get() {
if (MyAccountViewModels == null) {
MyAccountViewModels = MutableLiveData<My_account_base_response>()
loadviewmodel()
}
return MyAccountViewModels
}
private fun loadviewmodel(){
val token :String = SharedPrefManager.getInstance(getApplication()).user.access_token.toString()
RetrofitClient.instance.fetchUser(token)
.enqueue(object : Callback<My_account_base_response> {
override fun onFailure(call: Call<My_account_base_response>, t: Throwable) {
Log.d("res", "" + t)
}
override fun onResponse(
call: Call<My_account_base_response>,
response: Response<My_account_base_response>
) {
var res = response
if (res.body()?.status == 200) {
MyAccountViewModels!!.value = response.body()
} else {
try {
val jObjError =
JSONObject(response.errorBody()!!.string())
Toast.makeText(getApplication(),
jObjError.getString("user_msg"),
Toast.LENGTH_LONG).show()
} catch (e: Exception) {
Log.e("errorrr", e.message)
}
}
}
})
}}
There are bunch of things wrong here, so let me provide you refactored code and explanation as much as I would be able to..
Activity:
class MyAccount : BaseClassActivity() {
private val mActionBarToolbar by lazy { findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbartable) }
private val resetbutton by lazy { findViewById<Button>(R.id.resetpwd) }
private val editbutton by lazy { findViewById<Button>(R.id.editdetail) }
private val first_name by lazy { findViewById<TextView>(R.id.firstname) }
private val last_name by lazy { findViewById<TextView>(R.id.lastname) }
private val emailuser by lazy { findViewById<TextView>(R.id.emailuser) }
private val phone_no by lazy { findViewById<TextView>(R.id.phone_no) }
private val birthday by lazy { findViewById<TextView>(R.id.birthday) }
private val image by lazy { findViewById<ImageView>(R.id.imageprofile) }
lateinit var model: MyAccountViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.myaccount)
setSupportActionBar(mActionBarToolbar)
setEnabledTitle()
model = ViewModelProvider(this)[MyAccountViewModel::class.java]
resetbutton.setOnClickListener {
val i = Intent(applicationContext, ResetPasswordActivity::class.java)
startActivity(i)
}
editbutton.setOnClickListener {
val i = Intent(applicationContext, EditProfile::class.java)
startActivity(i)
}
model.accountResponseData.observe(this, object : Observer<My_account_base_response> {
override fun onChanged(t: My_account_base_response?) {
first_name.setText(t?.data?.user_data?.first_name)
last_name.setText(t?.data?.user_data?.last_name)
emailuser.setText(t?.data?.user_data?.email)
phone_no.setText(t?.data?.user_data?.phone_no).toString()
birthday.setText(t?.data?.user_data?.dob).toString()
Glide.with(applicationContext)
.load(t?.data?.user_data?.profile_pic)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_launcher_foreground)
.into(image)
}
})
}
override fun onResume() {
super.onResume()
model.loadAccountData()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpFromSameTask(this)
true
}
else -> super.onOptionsItemSelected(item)
}
}
}
Few notes on your activity class:
You don't need to findViewById everytime, just do it once during onCreate or do it lazily. (FYI consider using kotlin synthetics or view binding or data binding)
Initialize your viewModel during onCreate method only. (That's the best way to do it)
Also observer your LiveData from ViewModel once, it should be also from the onCreate as it's the entry point to the activity and apart from config changes this method called only once. So, it's safe to observe it over there rather than during onResume which will be called multiple times during activity lifecycle. (The main issue your code wasn't working, so as a fix you only call your API method from ViewModel during resume)
ViewModel:
class MyAccountViewModel(context: Application) : AndroidViewModel(context) {
private val _accountResponseData = MutableLiveData<My_account_base_response?>()
val accountResponseData: MutableLiveData<My_account_base_response?>
get() = _accountResponseData
init {
loadAccountData()
}
fun loadAccountData() {
val token: String = SharedPrefManager.getInstance(getApplication()).user.access_token.toString()
RetrofitClient.instance.fetchUser(token)
.enqueue(object : Callback<My_account_base_response> {
override fun onFailure(call: Call<My_account_base_response>, t: Throwable) {
Log.d("res", "" + t)
_accountResponseData.value = null
}
override fun onResponse(
call: Call<My_account_base_response>,
response: Response<My_account_base_response>
) {
var res = response
if (res.body()?.status == 200) {
_accountResponseData.value = response.body()
} else {
try {
val jObjError =
JSONObject(response.errorBody()!!.string())
Toast.makeText(
getApplication(),
jObjError.getString("user_msg"),
Toast.LENGTH_LONG
).show()
} catch (e: Exception) {
Log.e("errorrr", e.message)
}
}
}
})
}
}
Don't make initial API call along with LiveData creation, it's okay to do in most of cases but if you're updating LiveData on response of that call then it's good to make it separately like during init block.
It's good practice not to allow Ui (Activity/Fragments) to modify LiveDatas of ViewModel directly. So, that's good sign you're following such pattern by having private MutableLiveData exposed as public LiveData, but do it correctly as suggested.
Side note: Your view model doesn't need to be LifecycleObserver. LifecycleObserver is used for some custom class/component which needs to be managed by their self by silently observing/depending on activity lifecycle independently. That's not the use case of ViewModel.
The only thing that I found why your code wasn't working correctly is because you were creating & observing ViewModel & LiveData over & over again as new objects from onResume method where you called hello() method.
Let me know if something don't make sense or missing.

Unit test always fail when class is run

when I run this class, I'm always got test failed in method verify_on_success_is_called() with error,
Actually, there were zero interactions with this mock.
but if I run method only, test will passed.
#Mock
lateinit var mDummy: Dummy
private lateinit var mainViewModel: MainViewModel
#Mock
lateinit var main: MainViewModel.IMain
#Before
#Throws(Exception::class)
fun setup() {
MockitoAnnotations.initMocks(this)
MainViewModel.mIMain = main
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
mainViewModel = MainViewModel(mDummy)
}
#Test
fun verify_on_success_is_called() {
val mockList: ArrayList<Employee> = ArrayList()
mockList.add(Employee(1, "a", 20000.0, 22))
val list: List<Employee> = mockList
`when`(mDummy.getEmployees()).thenReturn(Observable.just(Response.success(list)))
mainViewModel.getEmployees()
Mockito.verify(main, times(1)).onSuccess()
}
#Test
fun verify_on_onError_is_called() {
MainViewModel.mIMain = main
`when`(mDummy.getEmployees()).thenReturn(Observable.error(Throwable()))
mainViewModel.getEmployees()
Mockito.verify(main, times(1)).onError()
}
this the viewModel class I want to test
class MainViewModel(private val mDummy: Dummy) : ViewModel() {
companion object {
lateinit var mIMain: IMain
}
interface IMain {
fun onSuccess()
fun onError()
}
fun getEmployees() {
mDummy.getEmployees()
.observeOn(SchedulerProvides.main())
.subscribeOn(SchedulerProvides.io())
.subscribe({ response ->
if (response.isSuccessful) {
mIMain.onSuccess()
} else {
mIMain.onError()
}
}, {
mIMain.onError()
})
}
and this my mainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
MainViewModel.mIMain = mIMainViewModelIniliazed()
}
private fun mIMainViewModelIniliazed() = object :MainViewModel.IMain{
override fun onSuccess() {
}
override fun onError() {
}
}
Please correct me if am wrong but i think your problem is because you're setting
MainViewModel.mIMain = main
before creating your viewmodel instance, shouldn't be as below?
mainViewModel = MainViewModel(mDummy)
mainViewModel.mIMain = main

How to get a variable from another class in kotlin?

I want to get a variable from an activity and use it in another class.
This variable will be filled by an user in a editText that is called editTextSerie
override fun searchSeries(listener: OnDataListener) {
val retrofit = Retrofit.Builder().addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://api.themoviedb.org/3/")
.build()
val client = retrofit.create(MovieDBApiInterface::class.java)
val objetoClasse1 = SearchActivity()
var nomeS = objetoClasse1.editTextSerie.text.toString().trim()
val responseCall = client.searchSeries("API_KEY", "pt-BR", nomeS)
responseCall.enqueue(object : Callback<AllSeriesResponse> {
override fun onResponse(call: Call<AllSeriesResponse>?, response1: Response<AllSeriesResponse>?) {
listener.onSuccess(response1!!.body()!!.results)
}
override fun onFailure(call: Call<AllSeriesResponse>?, t: Throwable?) {
listener.onFailure(t!!.message.toString())
}
})
}
This function "searchSeries" is from the class "Series".
I want to get the "editTextSerie" from another class called "Search Activity",
so i created the variable "nomeS" to receive the value of it.
class SearchActivity : AppCompatActivity() {
var botaoSearch: AppCompatImageButton? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
botaoSearch = findViewById(R.id.btn_search)
botaoSearch?.setOnClickListener {
var nomeSerie = editTextSerie.text.toString().trim()
}
}
}
I want to receive this value (value of editTextSerie comes from the XML of SearchActivity ) and use it at responseCall with the "nomeS" variable
What is OnDataListener? Not really sure it is interface or abstract class, so I' ll write some pseudo code.
First change your function searchSeries's params to
searchSeries(text: String, listener: OnDataListener)
So in the class Series, you can get the data in your function searchSeries:
override fun searchSeries(text: String, listener: OnDataListener) {
// ...
// you can get the "text" string
}
Then edit your SearActivity's listener:
class SearchActivity : AppCompatActivity() {
var botaoSearch: AppCompatImageButton? = null
// create class "Series"
val series = Series()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
botaoSearch = findViewById(R.id.btn_search)
botaoSearch?.setOnClickListener {
var nomeSeries = editTextSerie.text.toString().trim()
searchSeries(nomeSeries)
}
}
private fun searchSeries(text: String) {
series.searchSeries(text, object : OnDataListener {
override onSuccess(a0: ...) {
}
override onFailure(message: String) {
}
})
}
}
If OnDataListener is a abstract class:
series.searchSeries(text, object : OnDataListener() {
override onSuccess(a0: ...) {
}
override onFailure(message: String) {
}
})

Categories

Resources