How to pass context inside Retrofit interface - android

I want to pass header authorization token in HTTP requests in retrofit. The token is saved in DataStore. My problem is how to retrieve the token from DataStore and pass it to the intercepter header. I've spent hours thinking of a workaround and searching through the internet but I came with nothing. I'm also new to Kotlin. Here is the code snippet:
interface RoomAPIService {
#GET("rooms")
fun getAllRooms(#Header("Authorization") authHeader: String): Call<List<Room>>
var context: Context
companion object {
var retrofitService: RoomAPIService? = null
var token: String = ""
fun getInstance() : RoomAPIService {
GlobalScope.launch(Dispatchers.IO)
{
//How to pass context to DataRepository.getInstance(context)
token = DataStoreRepository.getInstance().getToken().toString()
}
val httpClient = OkHttpClient.Builder()
httpClient.addInterceptor { chain ->
val request = chain.request().newBuilder().addHeader("Authorization","Bearer " + theTokenRetrievedFromDataStore).build()
chain.proceed(request)
}
.
.
}
.
.
}
Here is DataStoreRepository.kt:
class DataStoreRepository(context: Context) {
private val dataStore: DataStore<Preferences> = context.createDataStore(
name = "token_store"
)
companion object {
private val TOKEN = preferencesKey<String>("TOKEN")
private var instance: DataStoreRepository? = null
fun getInstance(context: Context): DataStoreRepository {
return instance ?: synchronized(this) {
instance ?: DataStoreRepository(context).also { instance = it }
}
}
}
suspend fun savetoDataStore(token: String) {
dataStore.edit {
it[TOKEN] = token
}
}
suspend fun getToken(): String? {
val preferences: Preferences = dataStore.data.first()
Log.d("datastore", "token retrieved: ${preferences[TOKEN]} +++++++++++")
return preferences[TOKEN]
}
}
And here is MainActivity.kt:
class MainActivity : AppCompatActivity() {
private lateinit var logoutBtn: Button
private lateinit var bottomNavigation: BottomNavigationView
private lateinit var binding: ActivityMainBinding
lateinit var viewModel: RoomViewModel
private val retrofitService = RoomAPIService.getInstance(this)
val adapter = RoomsAdapter()
private var token: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
logoutBtn = binding.logoutBtn
bottomNavigation = binding.bottomNavigation
viewModel = ViewModelProvider(this, RoomViewModeFactory(RoomRepository(retrofitService))).get(RoomViewModel::class.java)
binding.recyclerview.adapter = adapter
var token = intent.getStringExtra("token")
Log.d("tokenCheck","checkToken: $token")
if (token != null) {
viewModel.getAllRooms(token)
}
..
Any help will be greatly appreciated!

You can change your getInstance() signature to contain Context object.
fun getInstance(context:Context) : RoomAPIService {
GlobalScope.launch(Dispatchers.IO)
{
token = DataRepository.getInstance(context).getToken().toString()
}
}

This is not a good practise to use "context" in a non-ui module in your application. Networking modules ( or Data layer you may say) should not know about android libraries and components. My suggestion is to use a value in an "variable" which can be changed in runtime and can be seen in the Network module then use it in the OkHttp interceptor. Finally, you just need to initialize the variable at the beginning of your application.

Related

Retrofit after adding Flow does not return values

Recently, I decided to learn a bit about how to write android apps. After read book and checked many codes, blogs etc. I prepared small code which should get a list of data from rest service and present them on a screen in recyclerView. It worked with "hardcoded data", after added retrofit I have seen the data in Log, because I used enqueue with onResponse method. But it is async call, therefore I added Flow with emit and collect methods to handle incoming data. Unfortunately, still it does not work, now even Log is empty.
interface ApiInterface {
#GET("/api/v1/employees")
fun getEmployees() : Call<ResponseModel>
}
object ServiceBuilder {
private val client = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor(HttpLoggingInterceptor.Logger.DEFAULT)
.setLevel(HttpLoggingInterceptor.Level.BODY))
.build()
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
fun<T> buildService(service: Class<T>): T{
return retrofit.create(service)
}
}
class EmployeeRepository() {
fun getEmployees(): Flow<ResponseModel?> = flow {
val response = ServiceBuilder.buildService(ApiInterface::class.java)
Log.d("restAPI",response.getEmployees().execute().body()!!.toString() )
emit( response.getEmployees().execute().body() )
}
}
class MainViewModel(private val savedStateHandle: SavedStateHandle): ViewModel() {
init {
viewModelScope.launch {
EmployeeRepository().getEmployees().collect {
Log.d("restAPI", it.toString())
}
}
}
}
class MainActivity : AppCompatActivity() {
private val mainModel: MainViewModel by viewModels()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
val employee = EmployeeModel(id = 1, employee_age = 11, employee_salary = 12,
employee_name = "ABCD", profile_image = "")
var employeeList = mutableListOf(employee)
val adapter = EmployeeListAdapter(employeeList)
binding.recyclerView.adapter = adapter
}
}
Maybe I missed something in the code or in logic, I cannot find helpful information in internet. Can anyone tell me what and how should I change my code?
UPDATE:
Thank you ho3einshah!
For everyone interested in now and in the future I'd like inform that change from Call to Response:
interface ApiInterface {
#GET("/api/v1/employees")
suspend fun getEmployees() : Response<ResponseModel>
}
and change init to getData method:
fun getData() = repository.getEmployees()
were clue to solve the issue.
Moreover I called livecycleScope one level above - in AppCompatActivity for passing data directly to adapter:
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mainModel.getData().collect { employeeList ->
Log.d("restAPI", employeeList.toString() )
val adapter = EmployeeListAdapter(employeeList)
binding.recyclerView.adapter = adapter
}
}
}
Now I see the list on screen with incoming data.
Hi I hope this answer help you.
first because of using GsonConverterFactory add this dependency to your build.gradle(app):
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
now change your api service to below code:
import retrofit2.Response
import retrofit2.http.GET
interface ApiInterface {
#GET("/api/v1/employees")
suspend fun getList() : Response<ResponseModel>
Please pay attention Response must be from retrofit2.Response
I have used the api you are using it. as a response you have a list with "data" json key. Create a Response model according to Json Response :
data class ResponseModel(
var status : String?,
var data : ArrayList<EmployeeModel>
)
Now this is EmployeeModel :
data class EmployeeModel(
var d:Long,
var employee_age:Long,
var employee_salary:Long,
var employee_name:String,
var profile_image:String
)
class EmployeeRepository {
fun getEmployees() = flow<Response<EmployeeModel>> {
val response = RetrofitBuilder.buildService(MainService::class.java).getEmployees()
Log.e("response",response.body()?.data.toString())
}
}
and for your viewModel its better to call repository from a method and not in init block :
class MainViewModel : ViewModel() {
private val repository = EmployeeRepository()
fun getData() {
viewModelScope.launch(Dispatchers.IO) {
val a = repository.getEmployees()
.collect{
}
}
}
}
and in your MainActivity initialize MainViewModel like this and call MainViewModel method:
class MainActivity : AppCompatActivity() {
lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mainViewModel = ViewModelProvider(this)[MainViewModel::class.java]
mainViewModel.getData()
}
}

Unable to invoke no-args constructor for retrofit2.Call MVVM Coroutines Retrofit

I want to use coroutines in my project only when I use coroutines I get the error :Unable to invoke no-args constructor. I don't know why it's given this error. I am also new to coroutines.
here is my apiclient class:
class ApiClient {
val retro = Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
Here is my endpoint class:
#GET("v2/venues/search")
suspend fun get(
#Query("near") city: String,
#Query("limit") limit: String = Constants.limit,
#Query("radius") radius: String = Constants.radius,
#Query("client_id") id: String = Constants.clientId,
#Query("client_secret") secret: String = Constants.clientSecret,
#Query("v") date: String
): Call<VenuesMainResponse>
my Repository class:
class VenuesRepository() {
private val _data: MutableLiveData<VenuesMainResponse?> = MutableLiveData(null)
val data: LiveData<VenuesMainResponse?> get() = _data
suspend fun fetch(city: String, date: String) {
val retrofit = ApiClient()
val api = retrofit.retro.create(VenuesEndpoint::class.java)
api.get(
city = city,
date = date
).enqueue(object : Callback<VenuesMainResponse>{
override fun onResponse(call: Call<VenuesMainResponse>, response: Response<VenuesMainResponse>) {
val res = response.body()
if (response.code() == 200 && res != null) {
_data.value = res
} else {
_data.value = null
}
}
override fun onFailure(call: Call<VenuesMainResponse>, t: Throwable) {
_data.value = null
}
})
}
}
my ViewModel class:
class VenueViewModel( ) : ViewModel() {
private val repository = VenuesRepository()
fun getData(city: String, date: String): LiveData<VenuesMainResponse?> {
viewModelScope.launch {
try {
repository.fetch(city, date)
} catch (e: Exception) {
Log.d("Hallo", "Exception: " + e.message)
}
}
return repository.data
}
}
part of activity class:
class MainActivity : AppCompatActivity(){
private lateinit var venuesViewModel: VenueViewModel
private lateinit var adapter: HomeAdapter
private var searchData: List<Venue>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val editText = findViewById<EditText>(R.id.main_search)
venuesViewModel = ViewModelProvider(this)[VenueViewModel::class.java]
venuesViewModel.getData(
city = "",
date = ""
).observe(this, Observer {
it?.let { res ->
initAdapter()
rv_home.visibility = View.VISIBLE
adapter.setData(it.response.venues)
searchData = it.response.venues
println(it.response.venues)
}
})
this is my VenuesMainResponse data class
data class VenuesMainResponse(
val response: VenuesResponse
)
I think the no-args constructor warning should be related to your VenuesMainResponse, is it a data class? You should add the code for it as well and the complete Log details
Also, with Coroutines you should the change return value of the get() from Call<VenuesMainResponse> to VenuesMainResponse. You can then use a try-catch block to get the value instead of using enqueue on the Call.
Check this answer for knowing about it and feel free to ask if this doesn't solve the issue yet :)
UPDATE
Ok so I just noticed that it seems that you are trying to use the foursquare API. I recently helped out someone on StackOverFlow with the foursquare API so I kinda recognize those Query parameters and the Venue response in the code you provided above.
I guided the person on how to fetch the Venues from the Response using the MVVM architecture as well. You can find the complete code for getting the response after the UPDATE block in the answer here.
This answer by me has code with detailed explanation for ViewModel, Repository, MainActivity, and all the Model classes that you will need for fetching Venues from the foursquare API.
Let me know if you are unable to understand it, I'll help you out! :)
RE: UPDATE
So here is the change that will allow you to use this code with Coroutines as well.
Repository.kt
class Repository {
private val _data: MutableLiveData<mainResponse?> = MutableLiveData(null)
val data: LiveData<mainResponse?> get() = _data
suspend fun fetch(longlat: String, date: String) {
val retrofit = Retro()
val api = retrofit.retro.create(api::class.java)
try {
val response = api.get(
longLat = longlat,
date = date
)
_data.value = response
} catch (e: Exception) {
_data.value = null
}
}
}
ViewModel.kt
class ViewModel : ViewModel() {
private val repository = Repository()
val data: LiveData<mainResponse?> = repository.data
fun getData(longLat: String, date: String) {
viewModelScope.launch {
repository.fetch(longLat, date)
}
}
}
api.kt
interface api {
#GET("v2/venues/search")
suspend fun get(
#Query("ll") longLat: String,
#Query("client_id") id: String = Const.clientId,
#Query("client_secret") secret: String = Const.clientSecret,
#Query("v") date: String
): mainResponse
}
MainActivity.kt
private val viewModel by viewModels<ViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.getData(
longLat = "40.7,-74",
date = "20210718" // date format is: YYYYMMDD
)
viewModel.data
.observe(this, Observer {
it?.let { res ->
res.response.venues.forEach { venue ->
val name = venue.name
Log.d("name ",name)
}
}
})
}
}

Koin DI Clear dependencies

I am passing the token to the GET method of the request using Koin. But after authorizing a new user, the old token is saved. To get a new access token, you need to exit the application, log in again and log in.
How do I get the Koin dependencies to be cleared when the Logout button is clicked?
val appModule = module {
factory { provideToken(provideSharedPreferences(androidContext())) }
}
private fun provideSharedPreferences(context: Context): SharedPreferences =
context.getSharedPreferences("token", Context.MODE_PRIVATE)
fun provideToken(sharedPreferences: SharedPreferences): String =
sharedPreferences.getString("key", "")
Inject token:
class VkRetrofitDataSource (
private val vkRetrofitApi: VkRetrofitApi,
private val ioDispatcher: CoroutineDispatcher,
) : VkNetworkDataSource, KoinComponent {
private val accessToken: String by inject()
override suspend fun getUserInfo(
fields: String,
apiVersion: String
): Result<ResponseResultUser> =
withContext(ioDispatcher) {
val response = vkRetrofitApi.getUserInfo(fields, apiVersion, accessToken)
val userResponse = response.body()
Timber.d("Token $userResponse")
return#withContext if (response.isSuccessful && userResponse != null) {
Result.Success(userResponse)
}
}
I think what you need is
private val accessToken: String get() = get()
So, every time you access this property, it will invoke that factory in Koin module

How do I test a ViewModel class which has an interface dependecy that is generated by android as a Unit Test

Good day all, am trying to test my ViewModel class and it has a dependency of datasource, I tried to mock this, but it won't work because it's an interface, I believe the interface implementation is generated at runtime, how do I unit test this class, below is my ViewModel class
class LoginViewModel #ViewModelInject constructor(#ApplicationContext private val context: Context,
private val networkApi: NetworkAPI,
private val dataStore: DataStore<Preferences>)
: ViewModel() {
val clientNumber = MutableLiveData<String>()
val clientPassword = MutableLiveData<String>()
private val _shouldNavigate = MutableLiveData(false)
val shouldNavigate: LiveData<Boolean>
get() = _shouldNavigate
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String>
get() = _errorMessage
private val _activateDeviceButton = MutableLiveData(false)
val activateButton : LiveData<Boolean>
get() = _activateDeviceButton
init {
populateApiWithFakeData()
}
suspend fun authenticateUsers(): Boolean {
val clientNumber = clientNumber.value
val clientPassword = clientPassword.value
requireNotNull(clientNumber)
requireNotNull(clientPassword)
val (userExist, token) = networkApi.doesUserExist(clientNumber.toLong(), clientPassword)
if (token.isNotBlank()) storeTokenInStore(token)
return if (userExist) {
true
} else {
_errorMessage.value = "Incorrect account details. Please try again with correct details"
false
}
}
private suspend fun storeTokenInStore(token: String) {
dataStore.edit { pref ->
pref[TOKEN_PREFERENCE] = token
}
}
and here is my ViewModel Test class
#Config(sdk = [Build.VERSION_CODES.O_MR1])
#RunWith(AndroidJUnit4::class)
class LoginViewModelTest{
private val context : Context = ApplicationProvider.getApplicationContext()
private val dataCentre = NetworkApImpl()
#Mock
private lateinit var dataStore: DataStore<Preferences>
#Before
fun setUpDataCenters(){
val loginData = DataFactory.generateLoginData()
for (data in loginData){
dataCentre.saveUserData(data)
}
}
#After
fun tearDownDataCenter(){
dataCentre.clearDataSet()
}
#Test
#ExperimentalCoroutinesApi
fun authenticateUser_shouldAuthenticateUsers(){
//Given
val viewModel = LoginViewModel(context, dataCentre, dataStore)
viewModel.clientNumber.value = "8055675745"
viewModel.clientPassword.value = "robin"
//When
var result : Boolean? = null
runBlocking {
result = viewModel.authenticateUsers()
}
//Then
Truth.assertThat(result).isTrue()
}
Any assistance rendered will be appreciated.
You can wrap your dependency in a class you own as Mockito suggests here. This also has the upside of letting you change your storage implementation latter without having and impact on every view model using it.

How to maintain Singleton class for Web Socket Connection in Android Hilt-dagger?

I am new to Android Dagger-Hilt and I found it useful for my project. However, recently I want to use this concept to get my ServerConnection class to become Singleton across different view (fragment and activity). How can I achieve that?
I had tried to approach below but I can't get it Singleton as it will create 2 ServerConnection instance in my fragment/activity view. Where had I do wrong?
Current approach
AppModule.kt
#Singleton
#Provides
fun provideSocketConnection(tokenDao: TokenDao) : ServerConnection{
val token = runBlocking(Dispatchers.IO) { tokenDao.find() }
val tok = token!!.token
val refreshToken = token.refresh_token
return ServerConnection(URL)
}
ServerConnection.kt
class ServerConnection(url: String) {
enum class ConnectionStatus {
DISCONNECTED, CONNECTED
}
interface ServerListener {
fun onNewMessage(message: String?)
fun onStatusChange(status: ConnectionStatus?)
}
private var mWebSocket: WebSocket? = null
private val mClient: OkHttpClient
private val mServerUrl: String
private var mMessageHandler: Handler? = null
private var mStatusHandler: Handler? = null
private var mListener: ServerListener? = null
init {
mClient = OkHttpClient.Builder()
.readTimeout(3, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
mServerUrl = url
}
private inner class SocketListener : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
val m = mStatusHandler!!.obtainMessage(0, ConnectionStatus.CONNECTED)
mStatusHandler!!.sendMessage(m)
}
override fun onMessage(webSocket: WebSocket, text: String) {
val m = mMessageHandler!!.obtainMessage(0, text)
mMessageHandler!!.sendMessage(m)
}
override fun onClosed(
webSocket: WebSocket,
code: Int,
reason: String
) {
val m =
mStatusHandler!!.obtainMessage(0, ConnectionStatus.DISCONNECTED)
mStatusHandler!!.sendMessage(m)
}
override fun onFailure(
webSocket: WebSocket,
t: Throwable,
response: Response?
) {
disconnect()
}
}
fun connect(listener: ServerListener?) {
val request = Request.Builder()
.url(mServerUrl)
.build()
mWebSocket = mClient.newWebSocket(request, SocketListener())
mListener = listener
mMessageHandler =
Handler(Handler.Callback { msg: Message ->
mListener?.onNewMessage(msg.obj as String)
true
})
mStatusHandler = Handler(Handler.Callback { msg: Message ->
mListener!!.onStatusChange(msg.obj as ConnectionStatus)
true
})
}
fun disconnect() {
mWebSocket?.cancel()
mListener = null
mMessageHandler?.removeCallbacksAndMessages(null)
mStatusHandler?.removeCallbacksAndMessages(null)
}
fun sendMessage(message: String?) {
mWebSocket!!.send(message!!)
}
}
View (Fragment/Activity)
#AndroidEntryPoint
class RoomFragment : Fragment(), ServerConnection.ServerListener {
#Inject lateinit var socketConnection: ServerConnection
}
You need to annotate your AppModule.kt class with #InstallIn(SinggltonComponent::class).
To know more about the hilt component, check this detail, here.

Categories

Resources