I'm trying to implement application, which will work with websocket. So I choose scarlet. I can see in logs a response from a server, but I cant consume a data in my viewModel. How to do that? I am using Koin + viewModel + coroutine
Module for Koin
val networkModule = module {
single { createScarlet() }
single <ChatSocketRepository> {
ChatSocketRepositoryImpl(get())
}
}
private fun createScarlet(): ChatSocketApi {
val client = OkHttpClient.Builder()
.readTimeout(DataProviderImplementation.TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(DataProviderImplementation.TIMEOUT, TimeUnit.SECONDS)
.connectTimeout(DataProviderImplementation.TIMEOUT, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
return Scarlet.Builder()
.webSocketFactory(client.newWebSocketFactory("wss://demos.kaazing.com/echo"))
.addMessageAdapterFactory(GsonMessageAdapter.Factory())
.addStreamAdapterFactory(CoroutinesStreamAdapterFactory())
.build()
.create()
}
ChatSocketApi
interface ChatSocketApi {
#Receive
fun observeText(): ReceiveChannel<String>
}
ChatSocketRepository
interface ChatSocketRepository {
fun observeTest(): ReceiveChannel<String>
}
ChatSocketRepositoryImpl:
class ChatSocketRepositoryImpl(private val api: ChatSocketApi) : ChatSocketRepository {
override fun observeTest(): ReceiveChannel<String> {
return api.observeText()
}
}
ViewModel
class MyViewModel(private val chatSocketRepository: ChatSocketRepository) : BaseViewModel() {
init {
viewModelScope.launch {
val text = chatSocketRepository.observeTest().consumeEach {
Log.d("SOCKET", it.toString())
}
}
}
you already get the data, and your data is String because you declare ReceiveChannel<String> you can use the received data and put it in a LiveData. change the value of livedata in the .consumeEach{} method instead.
Related
I am trying to perform a unit test and mock a retrofit call without success. When I run my test, I get only end printed. I should receive onResponse() printed as well.
The code works fine when I run my app, only the test does not call the mocked API call.
Method in ViewModel:
fun loadSensors() {
CoroutineScope(Dispatchers.IO).launch {
sensorsService.getUserSensors(getUserToken(), getUserId())
.enqueue(object : Callback<List<Long>> {
override fun onResponse(
call: Call<List<Long>>,
response: Response<List<Long>>
) {
println("onResponse()")
}
override fun onFailure(call: Call<List<Long>>, t: Throwable) {
println("onFailure()")
}
})
}
println("end")
}
Interface:
#GET("/sensors")
fun getUserSensors(): Call<List<Long>>
App module:
#Provides
#Singleton
fun provideRetrofitFactory(gsonConverterFactory: GsonConverterFactory): Retrofit {
val client = OkHttpClient.Builder().build()
return Retrofit.Builder()
.baseUrl("http://<url>")
.addConverterFactory(gsonConverterFactory)
.client(client)
.build()
}
Test:
#OptIn(DelicateCoroutinesApi::class)
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
#OptIn(ExperimentalCoroutinesApi::class)
#BeforeAll
fun beforeAll() {
Dispatchers.setMain(mainThreadSurrogate)
}
#Test
fun loadSensors() {
val mockedCall = mockk<retrofit2.Call<List<Long>>>()
every { mockedCall.enqueue(any()) } answers {
val callback = args[0] as retrofit2.Callback<List<Long>>
val response = retrofit2.Response.success(200, listOf(1L, 2L, 3L))
callback.onResponse(mockedCall, response)
}
every { sensorsService.getUserSensors(any(), any()) } answers {
mockedCall
}
}
I recommended that you see MockWebServer I am sure with use it you can do anything you have in your mind.
The question about post requests in android has been asked before, but all the solutions I've tried have not worked properly. On top of that, a lot of them seem to be overly complicated as well. All I wish to do is make a post to a specific sight with a few body parameters. Is there any simple way to do that?
Let me explain my request calling structure using Retrofit.
build.gradle(app)
// Retrofit + GSON
implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
ApiClient.kt
object ApiClient {
private const val baseUrl = ApiInterface.BASE_URL
private var retrofit: Retrofit? = null
private val dispatcher = Dispatcher()
fun getClient(): Retrofit? {
val logging = HttpLoggingInterceptor()
if (BuildConfig.DEBUG)
logging.level = HttpLoggingInterceptor.Level.BODY
else
logging.level = HttpLoggingInterceptor.Level.NONE
if (retrofit == null) {
retrofit = Retrofit.Builder()
.client(OkHttpClient().newBuilder().readTimeout(120, TimeUnit.SECONDS)
.connectTimeout(120, TimeUnit.SECONDS).retryOnConnectionFailure(false)
.dispatcher(
dispatcher
).addInterceptor(Interceptor { chain: Interceptor.Chain? ->
val newRequest = chain?.request()!!.newBuilder()
return#Interceptor chain.proceed(newRequest.build())
}).addInterceptor(logging).build()
)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
return retrofit
}
}
ApiClient will be used to initialize Retrofit singleton object, also initialize logging interceptors so you can keep track of the requests and responses in the logcat by using the keyword 'okhttp'.
SingleEnqueueCall.kt
object SingleEnqueueCall {
var retryCount = 0
lateinit var snackbar: Snackbar
fun <T> callRetrofit(
activity: Activity,
call: Call<T>,
apiName: String,
isLoaderShown: Boolean,
apiListener: IGenericCallBack
) {
snackbar = Snackbar.make(
activity.findViewById(android.R.id.content),
Constants.CONST_NO_INTERNET_CONNECTION, Snackbar.LENGTH_INDEFINITE
)
if (isLoaderShown)
activity.showAppLoader()
snackbar.dismiss()
call.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
hideAppLoader()
if (response.isSuccessful) {
retryCount = 0
apiListener.success(apiName, response.body())
} else {
when {
response.errorBody() != null -> try {
val json = JSONObject(response.errorBody()!!.string())
Log.e("TEGD", "JSON==> " + response.errorBody())
Log.e("TEGD", "Response Code==> " + response.code())
val error = json.get("message") as String
apiListener.failure(apiName, error)
} catch (e: Exception) {
e.printStackTrace()
Log.e("TGED", "JSON==> " + e.message)
Log.e("TGED", "Response Code==> " + response.code())
apiListener.failure(apiName, Constants.CONST_SERVER_NOT_RESPONDING)
}
else -> {
apiListener.failure(apiName, Constants.CONST_SERVER_NOT_RESPONDING)
return
}
}
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
hideAppLoader()
val callBack = this
if (t.message != "Canceled") {
Log.e("TGED", "Fail==> " + t.localizedMessage)
if (t is UnknownHostException || t is IOException) {
snackbar.setAction("Retry") {
snackbar.dismiss()
enqueueWithRetry(activity, call, callBack, isLoaderShown)
}
snackbar.show()
apiListener.failure(apiName, Constants.CONST_NO_INTERNET_CONNECTION)
} else {
retryCount = 0
apiListener.failure(apiName, t.toString())
}
} else {
retryCount = 0
}
}
})
}
fun <T> enqueueWithRetry(
activity: Activity,
call: Call<T>,
callback: Callback<T>,
isLoaderShown: Boolean
) {
activity.showAppLoader()
call.clone().enqueue(callback)
}
}
SingleEnqueueCall will be used for calling the retrofit, it is quite versatile, written with onFailure() functions and by passing Call to it, we can call an API along with ApiName parameter so this function can be used for any possible calls and by ApiName, we can distinguish in the response that which API the result came from.
Constants.kt
object Constants {
const val CONST_NO_INTERNET_CONNECTION = "Please check your internet
connection"
const val CONST_SERVER_NOT_RESPONDING = "Server not responding!
Please try again later"
const val USER_REGISTER = "/api/User/register"
}
ApiInterface.kt
interface ApiInterface {
companion object {
const val BASE_URL = "URL_LINK"
}
#POST(Constants.USER_REGISTER)
fun userRegister(#Body userRegisterRequest: UserRegisterRequest):
Call<UserRegisterResponse>
}
UserRegisterRequest.kt
data class UserRegisterRequest(
val Email: String,
val Password: String
)
UserRegisterResponse.kt
data class UserRegisterResponse(
val Message: String,
val Code: Int
)
IGenericCallBack.kt
interface IGenericCallBack {
fun success(apiName: String, response: Any?)
fun failure(apiName: String, message: String?)
}
MyApplication.kt
class MyApplication : Application() {
companion object {
lateinit var apiService: ApiInterface
}
override fun onCreate() {
super.onCreate()
apiService = ApiClient.getClient()!!.create(ApiInterface::class.java)
}
}
MyApplication is the application class to initialize Retrofit at the launch of the app.
AndroidManifest.xml
android:name=".MyApplication"
You have to write above tag in AndroidManifest inside Application tag.
MainActivity.kt
class MainActivity : AppCompatActivity(), IGenericCallBack {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val call = MyApplication.apiService.userRegister(UserRegisterRequest(email, password))
SingleEnqueueCall.callRetrofit(this, call, Constants.USER_REGISTER, true, this)
}
override fun success(apiName: String, response: Any?) {
val model = response as UserRegisterResponse
}
override fun failure(apiName: String, message: String?) {
if (message != null) {
showToastMessage(message)
}
}
}
Firstly, we create a call object by using the API defined in ApiInterface and passing the parameters (if any). Then using SingleEnqueueCall, we pass the call to the retrofit along with ApiName and the interface listener IGenericCallBack by using this. Remember to implement it to respective activity or fragment as above.
Secondly, you will have the response of the API whether in success() or failure() function overriden by IGenericCallBack
P.S: You can differentiate which API got the response by using the ApiName parameter inside success() function.
override fun success(apiName: String, response: Any?) {
when(ApiName) {
Constants.USER_REGISTER -> {
val model = response as UserRegisterResponse
}
}
}
The whole concept is to focus on reusability, now every API call has to create a call variable by using the API's inside ApiInterface then call that API by SingleEnqueueCall and get the response inside success() or failure() functions.
With the help of dagger and rxJava I update the list in a RecyclerView. Everything works well, the list is displayed. But the problem is that in the logs I see how this list is updated every second. What could be the problem? In a similar project but in Java everything works correctly, the list is updated once at startup.
My Network Module:
#Module(includes = [ViewModelModule::class])
class NetworkModule {
companion object {
const val KEY = "key"
const val BASE_URL = "base_url"
}
#Provides
#Singleton
fun provideOkHttp(): OkHttpClient {
val httpClient = OkHttpClient.Builder()
httpClient.addInterceptor(object : Interceptor {
#Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val original = chain.request()
val originalHttpUrl = original.url
val url = originalHttpUrl.newBuilder()
//.addQueryParameter("apikey", KEY)
.build()
val requestBuilder = original.newBuilder()
.url(url)
.header("apikey", KEY)
val request = requestBuilder.build()
return chain.proceed(request)
}
})
// logging interceptor
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
httpClient.addInterceptor(logging)
return httpClient.build()
}
#Provides
#Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(provideOkHttp())
.build()
}
#Provides
#Singleton
fun provideContactsService(retrofit: Retrofit) : ContactsService{
return retrofit.create(ContactsService::class.java)
}
}
My ViewModel:
class ContactsViewModel #Inject constructor(private val contactsRepository: ContactsRepository) :
ViewModel() {
var mutableLiveData = MutableLiveData<List<ContactsModel>>()
private val disposable = CompositeDisposable()
fun getContactMutableLiveData(): MutableLiveData<List<ContactsModel>> {
loadData()
return mutableLiveData
}
fun loadData() {
disposable.add(contactsRepository.modelSingle()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableSingleObserver<List<ContactsModel>>() {
override fun onSuccess(t: List<ContactsModel>) {
getContactMutableLiveData().value = t
}
override fun onError(e: Throwable) {
}
})
)
}
}
And my Activity:
contactsViewModel.getContactMutableLiveData().observe(this#ContactListActivity, Observer {
mAdapter = ContactsAdapter(this#ContactListActivity, it as ArrayList<ContactsModel>)
recycler_contacts.layoutManager =
LinearLayoutManager(applicationContext, OrientationHelper.VERTICAL, false)
recycler_contacts.adapter = mAdapter
recycler_contacts.setHasFixedSize(true)
mAdapter.sortByName()
})
Okay if you only want to update your data list only once... I would recommend you look into a single live event that would trigger the reloading of your recycler view
As such
//in repo
private SingleLiveEvent<Boolean> listHasBeenUpdate=new SingleLiveEvent<>();
//setItsGetterInTheRepo
public SingleLiveEvent<Boolean> getListHasBeenUpdated(){
return listHasBeenUpdated();
}
//uponSucessfuly fetching your list from retrofit
listHasBeenUpdated=true;
//pass list to viewmodel
then in the ViewModel, I would set the list to be an Observable Data which would be updated once it's fetched from retrofit (Consider using room db to store this)
//use a setter to set the list from Repo
ObservableField<List<Contacts>> list=new ObservableField<>();
public SingleLiveEvent<List<Contacts>> fetchContacts(){
return myRepo.getListHasBeenUpdated();
}
In your activity class now observe the single live event like so
viewModel.fetchContacts().observe(this,contacts->{
if(contacts){
//update Recycler
}
});
hope this helps you.
It was a logical error. You need to rewrite the loadData function as shown below
class ContactsViewModel #Inject constructor(private val contactsRepository: ContactsRepository) :
ViewModel() {
var mutableLiveData = MutableLiveData<List<ContactsModel>>()
private val disposable = CompositeDisposable()
fun getContactMutableLiveData(): MutableLiveData<List<ContactsModel>> {
disposable.add(contactsRepository.modelSingle()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableSingleObserver<List<ContactsModel>>() {
override fun onSuccess(t: List<ContactsModel>) {
mutableLiveData.value = t
}
override fun onError(e: Throwable) {
}
}))
return mutableLiveData
}
}
I am developing a new android app but I am getting the following exception Unable to invoke no-args constructor for kotlinx.coroutines.Deferred>. Registering an InstanceCreator with Gson for this type may fix this problem.
below my MainViewModel.kt
#Suppress("UNCHECKED_CAST")
class MainViewModel(val newsRepository: NewsRepository) : ViewModel(), CoroutineScope {
// Coroutine's background job
val job = Job()
// Define default thread for Coroutine as Main and add job
override val coroutineContext: CoroutineContext = Dispatchers.Main + job
val showLoading = MutableLiveData<Boolean>()
val sportList = MutableLiveData <List<Article>>()
val showError = SingleLiveEvent<String>()
fun loadNews() {
// Show progressBar during the operation on the MAIN (default) thread
showLoading.value = true
// launch the Coroutine
launch {
// Switching from MAIN to IO thread for API operation
// Update our data list with the new one from API
val result = withContext(Dispatchers.IO) {
newsRepository?.getNewsList()
}
// Hide progressBar once the operation is done on the MAIN (default) thread
showLoading.value = false
when (result) {
is UseCaseResult.Success<*> -> {
sportList.value = result.data as List<Article>
}
is Error -> showError.value = result.message
}
}
}
override fun onCleared() {
super.onCleared()
// Clear our job when the linked activity is destroyed to avoid memory leaks
job.cancel()
}
}
below appmodules.kt my network logic implementation
const val BASE_URL = "https://newsapi.org/"
val appModules = module {
// The Retrofit service using our custom HTTP client instance as a singleton
single {
createWebService<SportNewsInterface>(
okHttpClient = createHttpClient(),
factory = RxJava2CallAdapterFactory.create(),
baseUrl = BASE_URL
)
}
// Tells Koin how to create an instance of CatRepository
factory<NewsRepository> { (NewsRepositoryImpl(sportsNewsApi = get())) }
// Specific viewModel pattern to tell Koin how to build MainViewModel
viewModel { MainViewModel (newsRepository = get ()) }
}
/* Returns a custom OkHttpClient instance with interceptor. Used for building Retrofit service */
fun createHttpClient(): OkHttpClient {
val client = OkHttpClient.Builder()
client.readTimeout(5 * 60, TimeUnit.SECONDS)
return client.addInterceptor {
val original = it.request()
val requestBuilder = original.newBuilder()
requestBuilder.header("Content-Type", "application/json")
val request = requestBuilder.method(original.method, original.body).build()
return#addInterceptor it.proceed(request)
}.build()
}
/* function to build our Retrofit service */
inline fun <reified T> createWebService(
okHttpClient: OkHttpClient,
factory: CallAdapter.Factory, baseUrl: String
): T {
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.addCallAdapterFactory(factory)
.client(okHttpClient)
.build()
return retrofit.create(T::class.java)
}
below SportNewsResponse.kt
data class SportNewsResponse(
val articles: List<Article>,
val status: String,
val totalResults: Int
)
below SportNewsInterface where I have implemented my ending points
interface SportNewsInterface {
#GET("v2/top-headlines?country=us&apiKey=da331087e3f3462bb534b3b0917cbee9")
suspend fun getNews(): Deferred<List<SportNewsResponse>>
#GET("/v2/top-headlines?sources=espn&apiKey=da331087e3f3462bb534b3b0917cbee9")
fun getEspn(): Deferred<List<SportNewsResponse>>
#GET("/v2/top-headlines?sources=football-italia&apiKey=da331087e3f3462bb534b3b0917cbee9")
fun getFootballItalia(): Deferred<List<SportNewsResponse>>
#GET("/v2/top-headlines?sources=bbc-sport&apiKey=da331087e3f3462bb534b3b0917cbee9")
fun getBBCSport(): Deferred<List<SportNewsResponse>>
}
class Service{
interface RedditApi {
#GET("/top.json")
fun getTop(#Query("after") after: String,
#Query("limit") limit: String)
: Deferred<Response<News>>;
}
}
val okHttpClient = OkHttpClient.Builder()
.readTimeout(40, TimeUnit.SECONDS)
.addInterceptor { chain ->
val ongoing = chain.request().newBuilder()
ongoing.addHeader("Cache-Control", "no-cache")
ongoing.addHeader("User-Agent", System.getProperty("http.agent"))
//ongoing.addHeader("Authorization", val.trim());
chain.proceed(ongoing.build())
}
.connectTimeout(40, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()
.baseUrl( "/rest/s1/mobile/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.client(okHttpClient)
.build()
redditApi = retrofit.create(Service.RedditApi::class.java)
Okey I have that, am trying to use retrofit with Coroutine. I go to my activity and implement it like below.I get error dispatchers.main unresolved reference main.I am using kotlin 1.3.21. Also my other question is, what if user clicks back on the activity how can I cancel the coroutine operation?Like In Java I used to do call.cancel() with retrofit.It cancelled the call.
class MainActivity : AppCompatActivity(), Fightable {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
val request = App.redditApi.getTop("after", "limit")
withContext(Dispatchers.Main) {
try {
val response = request.await()
if (response.isSuccessful) {
val news: News? = response.body()
//Do something with response e.g show to the UI.
} else {
}
} catch (e: HttpException) {
} catch (e: Throwable) {
}
}
}}}
You need to create a single instance of coroutine context and also have a job defined to it.
val job = Job()
val coroutineScope = CoroutineContext(Dispatchers.Main+job)
And start the work using the declared scope and when you want to cancel the work, you can simply call job.cancel() which cancels all current and upcoming works.