I'm trying to fetch some data with Retrofit on my Android project update this on the ViewModel and my activity with LiveData.
Here is my service Class:
class PaymentService {
private var paymentMethodList = ArrayList<PaymentMethodModel>()
private val paymentMethodListLiveData = MutableLiveData<List<PaymentMethodModel>>()
init {
paymentMethodListLiveData.value = paymentMethodList
}
fun fetchPaymentMethods() {
val retrofit = Retrofit.Builder()
.baseUrl(SERVICE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(PaymentClient::class.java)
val jsonCall = service.getListOfPaymentMethods()
jsonCall.enqueue(object : Callback<List<PaymentMethodModel>> {
override fun onResponse(call: Call<List<PaymentMethodModel>>, response: Response<List<PaymentMethodModel>>) {
paymentMethodList = (response.body() as ArrayList<PaymentMethodModel>?)!!
}
override fun onFailure(call: Call<List<PaymentMethodModel>>, t: Throwable) {
//TODO
}
})
}
And here is where I'm trying to listen to the changes on the list:
goToNextButton.setOnClickListener {
paymentMethods = PaymentMethodSelectionViewModel().getAllPaymentMethods()
paymentMethods!!.observe(viewLifecycleOwner, Observer {
Log.e("", "")
})
}
The problem is that so far I'm getting the list only the first time with 0 elements and this observer method is not getting called after the rest call is made and the list updated.
Edit
class PaymentRepository {
private val paymentService = PaymentService()
fun getPaymentMethods(): LiveData<List<PaymentMethodModel>> {
paymentService.fetchPaymentMethods()
return paymentService.getPaymentMethods()
}
}
class PaymentMethodSelectionViewModel: ViewModel() {
private val paymentRepository = PaymentRepository()
private val paymentMethods = paymentRepository.getPaymentMethods()
fun getAllPaymentMethods(): LiveData<List<PaymentMethodModel>> {
paymentRepository.getPaymentMethods()
return paymentMethods
}
}
Change your request into viewmodel
class PaymentMethodSelectionViewModel: ViewModel() {
//Data
var paymentMethodList = MutableLiveData<List<PaymentMethodModel>>()
fun getAllPayments(){
val retrofit = Retrofit.Builder()
.baseUrl(SERVICE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(PaymentClient::class.java)
val jsonCall = service.getListOfPaymentMethods()
jsonCall.enqueue(object : Callback<List<PaymentMethodModel>> {
override fun onResponse(call: Call<List<PaymentMethodModel>>, response: Response<List<PaymentMethodModel>>) {
var data: List<PaymentMethodModel> = (response.body() as ArrayList<PaymentMethodModel>?)!!
paymentMethodList.value=data
}
override fun onFailure(call: Call<List<PaymentMethodModel>>, t: Throwable) {
//TODO
}
})
}
}
in your view (activity) use
//load
paymentMethodSelectionViewModel.getAllPayments();
//Observers
paymentMethodSelectionViewModel.paymentMethodList.observe(this,
Observer { list ->
// your code
})
I recommend you use retrofit 2 with corutines or RXJAVA2,
check this tutorial
https://medium.com/#amtechnovation/android-architecture-component-mvvm-part-1-a2e7cff07a76
https://medium.com/#saquib3705/consuming-rest-api-using-retrofit-library-with-the-help-of-mvvm-dagger-livedata-and-rxjava2-in-67aebefe031d
As #tyczj says in the comment, every time you use a LiveData, you have to decide when all the observers receive an update notification.
You can do this notification by calling post function of your paymentMethodListLiveData object. This is the correct way to use LiveData in Java.
In Kotlin I think you have to add something like this on your onResponse method:
paymentMethodListLiveData.value = paymentMethodList;
to implicitly call the post method and trigger methods in your observe function.
Hope this help or give you some hints.
Cheers
Related
I am trying to make an API call with Retrofit 2.9.0 and use the response in a Jetpack Compose activity. The problem is that the call repeats to infinite and updates the UI on every frame. I have never used the retrofit library before so I'm sorry if the code is messy. I will attach a GIF with how the app behaves and code snippets for everything. Thanks for reading this and wanting to help. <3
What is happening in the app
Here is the class that makes the call to the API:
public class ApiHandler {
Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.quotable.io/").build();
ApiInterface apiInterface = retrofit.create(ApiInterface.class);
Call<ResponseBody> call = apiInterface.getData();
public void getQuoteRaw()
{
call.enqueue(new Callback<ResponseBody>() {
#Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
try {
MainActivityKt.getModel().setQuoteInfo(response.body().string());
} catch (IOException e)
{
e.printStackTrace();
}
}
#Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
System.out.println("ERROR");
}
});
}
The Interface I use to define the getData() method:
public interface ApiInterface {
#GET("random")
Call<ResponseBody> getData();
The ViewModel class for the Compose Activity:
class MainViewModel: ViewModel() {
var quoteInfo by mutableStateOf("")
The Jetpack Compose activity:
val model = MainViewModel()
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
QuoteappTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Greeting()
}
}
}
}
}
#Composable
fun Greeting(viewModel: MainViewModel = model) {
val apihandle = ApiHandler()
apihandle.getQuoteRaw()
Text(text = viewModel.quoteInfo)
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
QuoteappTheme {
Greeting()
}
}
I am struggling with viewmodel injection. I have been following tutorials and changed the code a little bit in order to adjust it to my needs, but the app crashes.
I have App class holding my DaggerComponent with it's modules. Inside it's onCreate I have:
component = DaggerAppComponent.builder().daoModule(DaoModule(this)).build()
My AppModule:
#Singleton
#Component(modules = [DaoModule::class, ViewModelModule::class])
interface AppComponent {
val factory: ViewModelFactory
}
ViewModelModule :
#Module
abstract class ViewModelModule {
#Binds
#Singleton
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#Singleton
#IntoMap
#ViewModelKey(TaskViewModel::class)
abstract fun splashViewModel(viewModel: TaskViewModel): ViewModel
}
MyFactory:
#Singleton
class ViewModelFactory #Inject constructor(
private val viewModels: MutableMap<Class<out ViewModel>,
#JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
viewModels[modelClass]?.get() as T
}
I used here ViewModelKey, ViewModelModule and Factory, and Fragment extension function to perform Fragment viewmodel injection. I found it online and used it succesfuly on previous projects. This is my util function:
#MainThread
inline fun <reified VM : ViewModel> Fragment.daggerViewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this }
) = createViewModelLazy(
VM::class,
{ ownerProducer().viewModelStore },
{ App.component.factory }
)
And my DaoModule.
#Module
class DaoModule(private val app: Application) {
#Provides
#Singleton
fun getDB(): TaskDatabase = TaskDatabase.getAppDatabase(context())
#Provides
#Singleton
fun context(): Context = app.applicationContext
#Provides
fun gettaskDao(taskDatabase: TaskDatabase) : TaskDao = taskDatabase.TaskDao()
}
My entity:
#Entity(tableName = "userinfo")
data class Task(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "id") val id: Int = 0,
#ColumnInfo(name = "name") val name: String,
#ColumnInfo(name = "email") val email: String,
#ColumnInfo(name = "phone") val phone: String?
)
My TaskDatabase as follows:
#Database(entities = [Task::class], version = 1)
abstract class TaskDatabase : RoomDatabase() {
abstract fun TaskDao(): TaskDao
companion object {
private var INSTANCE: TaskDatabase? = null
fun getAppDatabase(context: Context): TaskDatabase {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.applicationContext, TaskDatabase::class.java, "AppDBB"
)
.allowMainThreadQueries()
.build()
}
return INSTANCE!!
}
}
}
My Dao interface.
#Dao
interface TaskDao {
#Query("SELECT * FROM userinfo")
fun getAllTaskInfo(): List<Task>?
#Insert
fun insertTask(user: Task?)
#Delete
fun deleteTask(user: Task?)
#Update
fun updateTask(user: Task?)
}
And now I have a logic to init my TaskViewModel inside my Fragment and attach observer to my Task List. However the app crashes.
Inside my fragment I have:
val viewModel: TaskViewModel by daggerViewModels { requireActivity() }
and also:
DaggerFragmentComponent
.builder()
.appComponent((requireActivity().application as App).getAppComponent())
.build()
.inject(this)
viewModel.allTaskList.observe(viewLifecycleOwner) {
// textView.text = it.toString()
}
and my TaskViewModel class is as follows:
class TaskViewModel #Inject constructor(var taskDao: TaskDao) : ViewModel() {
private var _allTaskList = MutableLiveData<List<Task>>()
val allTaskList = _allTaskList as LiveData<List<Task>>
init {
getAllRecords()
}
private fun getAllRecords() = _allTaskList.postValue(taskDao.getAllTaskInfo())
fun insertTask(task: Task) {
taskDao.insertTask(task)
getAllRecords()
}
}
Now I understand that this is A LOT of code, but can somebody help me figure this out? The dagger sees it's graph as I can build the project, so all the dependencies are provided. What I did wrong here? My logcat:
I found the solution myself. This was missing.
implementation 'androidx.room:room-runtime:2.5.0-alpha01'
searched on many sites and in various articles, but without effect, so I ask for your help with Dagger 2 (DI)
I can not understand what the problem is
I'm new to Dagger 2 (Di) and I'm just studying it, please help me understand
My code:
MainActivity.kt
class MainActivity : AppCompatActivity() {
#Inject
lateinit var networkService: NetworkService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
App().gerAppComponents().inject(mainActivity = this)
setContentView(R.layout.activity_main)
networkService.buildService(service = Movies::class.java).getPopularMovie(apiKey = "test", language = "en-US", page = 1)
}
}
App.kt
class App : Application() {
fun gerAppComponents(): AppComponent {
return DaggerAppComponent.builder().appNetworkModule(AppNetworkModule())
.build()
}
}
ServiceBuild.kt
object ServiceBuild : NetworkService {
private val client = OkHttpClient.Builder().build()
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://api.themoviedb.org/3")
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
override fun <T> buildService(service: Class<T>): T = retrofit.create(service)
}
NetworkService.kt
interface NetworkService {
fun <T> buildService(service: Class<T>): T
}
AppNetworkModule.kt
#Module
class AppNetworkModule {
#Provides
#Singleton
fun getRetrofitService(): ServiceBuild {
return ServiceBuild
}
}
AppComponent.kt
#Singleton
#Component(modules = arrayOf(AppNetworkModule::class))
interface AppComponent {
fun inject(mainActivity: MainActivity)
}
Build error
app/build/tmp/kapt3/stubs/debug/com/rebus/client/di/components/AppComponent.java:10: error: [Dagger/MissingBinding] com.rebus.client.services.impl.NetworkService cannot be provided without an #Provides-annotated method.
public abstract void inject(#org.jetbrains.annotations.NotNull()
^
com.rebus.client.services.impl.NetworkService is injected at
com.rebus.client.MainActivity.networkService
com.rebus.client.MainActivity is injected at
com.rebus.client.di.components.AppComponent.inject(com.rebus.client.MainActivity)
Change your code ServiceBuild to NetworkService like this:
#Module
class AppNetworkModule {
#Provides
#Singleton
fun getRetrofitService(): NetworkService {
return ServiceBuild
}
}
I am using MVVM pattern in my app I have separate repository class for network operations. In repository class I am getting response from the server. How can I show Toast message send from the server in my main activity.
Below is my code:
Repository.java
public class MyRepository {
MutableLiveData<List<Facts>> mutableLiveData = new MutableLiveData<>();
Application application;
public MyRepository(Application application) {
this.application = application;
}
public MutableLiveData<List<Facts>> getMutableLiveData(){
Retrofit retrofit = RetrofitClient.getInstance();
ApiService apiService = retrofit.create(ApiService.class);
apiService.getFacts().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<List<Facts>>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(List<Facts> facts) {
if(facts.size() > 0 && facts != null){
mutableLiveData.setValue(facts);
}
}
#Override
public void onError(Throwable e) {
TastyToast.makeText(application,e.getMessage(),TastyToast.LENGTH_SHORT,
TastyToast.ERROR).show();
}
#Override
public void onComplete() {
}
});
return mutableLiveData;
}
}
FactsViewModel.java
public class FactsViewModel extends AndroidViewModel {
MyRepository repo;
public FactsViewModel(#NonNull Application application) {
super(application);
repo = new MyRepository(application);
}
public LiveData<List<Facts>> getAllFacts(){
return repo.getMutableLiveData();
}
}
MainActivity.java
private void myFacts(){
FactsViewModel viewModel = new ViewModelProvider(this).get(FactsViewModel.class);
viewModel.getAllFacts().observe(this, new Observer<List<Facts>>() {
#Override
public void onChanged(List<Facts> facts) {
adapter = new FactsAdapter(facts,getActivity());
recycle.setAdapter(adapter);
}
});
}
How can I show error toast messages in MainActivity?
To implement that you firstly need to create a class which has the status of the response ,
Loading which is before the fetching of the data and there you can set progress bar to visible then on success you would set the data to your adapter and right after your hide your progress bar and in the on failure one , you show the toast message error
This is the generic class
class AuthResource<T>(
var authStatus : AuthStatus? = null,
var data : T,
var msg : String? = null
)
fun <T> success(#Nullable data: T): AuthResource<T> {
return AuthResource(
AuthStatus.Success,
data,
null
)
}
fun <T> Error(#NonNull msg: String?, #Nullable data: T) : AuthResource<T>? {
return AuthResource(
AuthStatus.ERROR,
data,
msg
)
}
fun <T> loading(#Nullable data: T): AuthResource<T>? {
return AuthResource(
AuthStatus.LOADING,
data,
null
)
}
enum class AuthStatus {
Success, ERROR, LOADING
}
This is my view model where i implement the authResource with the api response
class MainViewModel #Inject constructor( private var webAuth: WebAuth,
private var favFoodDao: FavFoodDao,
private var application: Application) : ViewModel() {
/// you have to create MediatorLiveData with authresource which contains your modelclass
private var mediatorLiveData = MediatorLiveData<AuthResource<WrapLatestMeals>>()
///Here you return a livedata object
fun ObserverCountries(): LiveData<AuthResource<WrapCountries>> {
var liveData = LiveDataReactiveStreams.fromPublisher(
webAuth.getCountries()
///onerrorreturn , rxjava operator which returns error in case
///of response failure
.onErrorReturn(object : Function<Throwable, WrapCountries> {
override fun apply(t: Throwable): WrapCountries {
var country = WrapCountries()
return country
}
})
.map(object : Function<WrapCountries,
AuthResource<WrapCountries>> {
override fun apply(t: WrapCountries):
AuthResource<WrapCountries> {
if(t.meals.isNullOrEmpty())
{
return Error(
"Error",
t
)!!
}
return success(t)
}
})
.subscribeOn(Schedulers.io())
)
//add that data to mediatorLivedata
mediatorLiveDataCountries.addSource(liveData, Observer {
mediatorLiveDataCountries.postValue(it)
mediatorLiveDataCountries.removeSource(liveData)
})
return mediatorLiveDataCountries
}
This is how you handle the status in your MainActivity
mainViewModel = ViewModelProvider(this,provider)[MainViewModel::class.java]
mainViewModel.ObserverCountries().observe(viewLifecycleOwner, Observer {
when(it.authStatus) {
AuthStatus.LOADING -> /// here you show progressbar in response pre-fetch
{
countriesFragmentBinding.countryprogress.show()
}
AuthStatus.Success -> { // here you update your ui
countriesAdapter = CountriesAdapter(it.data.meals!!,
requireContext())
countriesFragmentBinding.recyclercountries.adapter = countriesAdapter
countriesAdapter!!.deleteCategory(23)
countriesFragmentBinding.countryprogress.hide()
}
AuthStatus.ERROR -> // here you hide your progressbar and show your toast
{
countriesFragmentBinding.countryprogress.hide()
ToastyError(requireContext(),getString(R.string.errorretreivingdata))
}
}
})
return countriesFragmentBinding.root
}
}
I am using a paging library from Android Architecture Components.
Paging is implemented using ItemKeyedDataSource
class MyDatasource(
private val queryMap: HashMap<String, String>) : ItemKeyedDataSource<String, Order>() {
private val compositeDisposable: CompositeDisposable by lazy { CompositeDisposable() }
override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<Order>) {
compositeDisposable.add(
MyService.getService().fetchData(queryMap)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableObserver<OrdersResponse>() {
override fun onNext(orders: OrdersResponse) {
callback.onResult(orders.data)
}
override fun onError(e: Throwable) {
e.printStackTrace()
}
override fun onComplete() {
}
})
)
}
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<Order>) {
// do nothing
}
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<Order>) {
queryMap["offsetOrderId"] = params.key
compositeDisposable.add(
MyService.getService().fetchData(queryMap)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableObserver<OrdersResponse>() {
override fun onNext(orders: OrdersResponse) {
callback.onResult(orders.data)
}
override fun onError(e: Throwable) {
}
override fun onComplete() {
}
})
)
}
override fun getKey(item: Order): String {
return item.orderId
}
}
I build pagedlist in my viewmodel
class MyViewModel() : ViewModel() {
private var myPagingConfig: PagedList.Config? = null
var dataList: LiveData<PagedList<Order>>? = null
fun getOrders(params: HashMap<String, String>) {
if (myPagingConfig == null) {
myPagingConfig = PagedList.Config.Builder()
.setPageSize(LIMIT)
.setPrefetchDistance(10)
.setEnablePlaceholders(false)
.build()
}
dataList = LivePagedListBuilder(MyDataFactory(
MyDatasource(params)), myPagingConfig!!)
.setInitialLoadKey(null)
.setFetchExecutor(Executors.newFixedThreadPool(5))
.build()
}
}
However, when I observe the dataList in my activity, it sometimes (most of the times) returns an empty list, while in logcat I see that I had fetched data successfully. callback.onResult is invoked after it returns an empty list, but observer never gets notified again.
Can you tell me if what would cause this?