I'm creating an app where the user has to insert a serverurl in an EditText field, and that url should be the baseUrl of the retrofit-request.
So, my code works as it should when i use a hardcoded baseurl, but the app crashes when I try to pass the value from the Edittext to the baseUrl.
Thats how I tried to pass the value:
object NetworkLayer {
var newUrl: String = ""
val retrofit: Retrofit
get() = Retrofit.Builder()
.baseUrl(newUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
val myApi: MyApi by lazy {
retrofit.create(MyApi::class.java)
}
val apiClient = ApiClient(myApi)
}
and in my MainActivity:
var serverUrl = binding.et1.text.toString()
button.setOnClickListener {
NetworkLayer.newUrl = serverUrl
viewModel.getServerInformation(headerValue)
}
I get this error message: Error message: Caused by: java.lang.IllegalArgumentException: Expected URL scheme 'http' or 'https' but no scheme was found for.
So probably retrofit uses the empty "" string for the request. Somehow I should send the information to retrofit that when clicking the button the url from the Edittext (et1) is the baseUrl. When I use a seperate class (f.e. class Constants, with a companion object with a const val baseUrl = "hardcoded url") it works also.
Can I create a function to inform the retrofit client to use the Edittext as baseUrl and declare it in the onClickListener? or could it be a way to create the retrofit client in a class instead of an object? (using url: String as parameter in the class and adding the edittext as argument in the MainActivity?)
Sadly the #Url annotation for Retrofit doesn't work as I have to use also #Header and #Query in the different requests.
Or is there a compeletey different way for doing this?
Hopefully there is someone who can help me.
I managed to solve it, the only thing I had to change was:
val url = binding.etServerUrl.text instead of
val url = binding.etServerUrl.text.toString()
and when calling the function on button click I added the toString() to the url argument. When I try to add the toString() to the val url as I always did before it doesn't work, anyone can tell me why?
Here is an example how I use it (I changed the Retrofit client a bit to my first version in the question). So finally I can go ahead with my app, as I was blocked now for a few weeks with this.. :-)
object RetrofitClient{
var retrofitService: MyApi? = null
fun getInstance(url: String): MyApi{
if (retrofitService == null) {
val retrofit = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofitService = retrofit.create(MyApi::class.java)
}
return retrofitService!!
}
}
I changed the retrofitclient a bit, but it wors
Then in the repository:
class MainRepository (){
suspend fun getToken(cookie: String, url: String): TokenResponse? {
val request = RetrofitClient.getInstance(url).getToken(cookie)
if (request?.isSuccessful!!) {
return request.body()!!
}
return null
}
}
Viewmodel:
class SharedViewModel() : ViewModel() {
private val repository = MainRepository()
private val _getTokenLiveData = MutableLiveData<TokenResponse>()
val getTokenLiveData: LiveData<TokenResponse> = _getTokenLiveData
fun getToken(cookie: String, url: String) {
viewModelScope.launch {
val response = repository.getToken(cookie, url)
_getTokenLiveData.postValue(response)
}
}
}
And finally the MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
val viewModel: SharedViewModel by lazy {
ViewModelProvider(this)[SharedViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) //initializing the binding class
setContentView(binding.root)
val url = binding.etServerUrl.text
val headerValue = binding.etMac.text.toString()
val button = binding.button
val textView = binding.textView
button.setOnClickListener {
viewModel.getToken(headerValue, url = url.toString())
}
viewModel.getTokenLiveData.observe(this) { response ->
if (response == null) {
Toast.makeText(this#MainActivity, "Fehlerhaft", Toast.LENGTH_SHORT).show()
return#observe
}
textView.text = response.js.token
}
}
}
Related
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()
}
}
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.
I'm building an app for a company using MVVM & clean architecture so I've created 3 modules, the app module (presentation layer), the data module (data layer) & the domain module (domain/interactors layer). Now, in my data module, I'm using Retrofit and Gson to automatically convert the JSON I'm receiving from a login POST request to my kotlin data class named NetUserSession that you see below. The problem I'm having is that the logging interceptor prints the response with the data in it normally but the response.body() returns an empty NetUserSession object with null values which makes me think that the automatic conversion isn't happening for some reason. Can somebody please tell me what I'm doing wrong here?
KoinModules:
val domainModule = module {
single<LoginRepository> {LoginRepositoryImpl(get())}
single { LoginUseCase(get()) }
}
val presentationModule = module {
viewModel { LoginViewModel(get(),get()) }
}
val dataModule = module {
single { ApiServiceImpl().getApiService() }
single { LoginRepositoryImpl(get()) }
}
}
Api interface & retrofit:
interface ApiService {
#POST("Login")
fun getLoginResult(#Body netUser: NetUser) : Call<NetUserSession>
#GET("Books")
fun getBooks(#Header("Authorization") token:String) : Call<List<NetBook>>
}
class ApiServiceImpl {
fun getApiService(): ApiService {
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
//TODO:SP Remove the interceptor code when done debugging
val client: OkHttpClient = OkHttpClient.Builder()
.addInterceptor(logging)
.build()
val retrofit = Retrofit.Builder().baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
// tell retrofit to implement the interface of our api
return retrofit.create(ApiService::class.java)
}
}
NetUserSession:
data class NetUserSession(
#SerializedName("expires_in")
val expires_in: Int,
#SerializedName("token_type")
val token_type: String,
#SerializedName("refresh_token")
val refresh_token: String,
#SerializedName("access_token")
val access_token: String
) {
fun toUserSession(): UserSession = UserSession(
expiresIn = expires_in,
tokenType = token_type,
refreshToken = refresh_token,
accessToken = access_token
)
}
UserSession in domain:
data class UserSession(
val expiresIn:Int,
val tokenType:String,
val refreshToken:String,
val accessToken:String
)
LoginRepositoryImpl where the error occurs:
class LoginRepositoryImpl(private val apiService: ApiService) : LoginRepository {
override suspend fun login(username:String,password:String): UserSession? = withContext(Dispatchers.IO){
val response = apiService.getLoginResult(NetUser(username,password)).awaitResponse()
println("THE RESPONSE WAS : ${response.body()}")
return#withContext if(response.isSuccessful) response.body()?.toUserSession() else null
}
}
LoggingInterceptor result after the 200-OK:
{"expires_in":3600,"token_type":"Bearer","refresh_token":"T1amGR21.IdKM.5ecbf91162691e15913582bf2662e0","access_token":"T1amGT21.Idup.298885bf38e99053dca3434eb59c6aa"}
Response.body() print result:
THE RESPONSE WAS : NetUserSession(expires_in=0, token_type=null, refresh_token=null, access_token=null)
Any ideas what I'm failing to see here?
After busting my head for hours, the solution was to simply change the model class's members from val to var like so :
data class NetUserSession(
#SerializedName("expires_in")
var expires_in: Int = 0,
#SerializedName("token_type")
var token_type: String? = null,
#SerializedName("refresh_token")
var refresh_token: String? = null,
#SerializedName("access_token")
var access_token: String? = null
) {
fun toUserSession(): UserSession = UserSession(
expiresIn = expires_in,
tokenType = token_type!!,
refreshToken = refresh_token!!,
accessToken = access_token!!
)
}
I am trying to change the Retrofit baseUrl from SharedPreferences in my app at runtime, but the change is only implemented when I close and open the app. I have tried using onSharedPreferenceChangeListener() and onPreferenceChangeListener() but I still get the same result. How do I implement the listeners so that they change the baseUrl at runtime?
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(CompanyApiService .apiBaseUrl)
.build()
interface CompanyApiService {
#GET("employees")
fun getEmployeesAsync(): Deferred<List<Employees>>
#GET("title/{id}")
fun getTitlesAsync(#Path("id") id: Int): Deferred<List<Titles>>
#POST("message")
fun submitMessage(#Body message: Message): Call<String>
}
object CompanyApi {
val retrofitService: CompanyApiService by lazy {
retrofit.create(CompanyApiService ::class.java)
}
var apiBaseUrl = ""
}
MainActivity.kt
class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
...
PreferenceManager.setDefaultValues(this, R.xml.main_preference, false)
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
sharedPrefs.registerOnSharedPreferenceChangeListener(this)
val apiBaseUrl = sharedPrefs.getString(KEY_PREF_BASE_URL, "")
CompanyApi.apiBaseUrl = apiBaseUrl!!
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == KEY_PREF_BASE_URL) {
val newApiBaseUrl = sharedPreferences?.getString(key, "")
CompanyApi.apiBaseUrl = newApiBaseUrl!!
}
}
object CompanyApi {
val retrofitService: CompanyApiService by lazy {
retrofit.create(CompanyApiService ::class.java)
}
This creates a singleton, you need to change that and re-create your Api when you changed your base_url however I wouldn't advise to do so. Creating a retrofit instance is consuming and you might get into errors later on.
Lucky for you Retrofit has a simple solution for that:
public interface UserManager {
#GET
public Call<ResponseBody> userName(#Url String url);
}
The URL String should specify the full Url you wish to use.
also, check this out -> enter link description here
I have this test class:
class InspirationalQuoteInstrumentedTest {
private lateinit var server: MockWebServer
#Rule
#JvmField
val mActivityRule: ActivityTestRule<InspirationalQuoteActivity> = ActivityTestRule(InspirationalQuoteActivity::class.java)
#Before
fun setUp() {
server = MockWebServer()
server.start()
Constants.BASE_URL = server.url("/").toString()
}
#After
fun tearDown() {
server.shutdown()
}
#Test
fun ensureTheQuoteOfTheDayIsDisplayed() {
println("Base URL: ${Constants.BASE_URL}")
Log.e(TAG,"Base URL: ${Constants.BASE_URL}")
val response200 = this::class.java.classLoader.getResource("200.json").readText()
val jsonResponse = JSONObject(response200)
val expectedQuote = jsonResponse
.getJSONObject("contents")
.getJSONArray("quotes")
.getJSONObject(0)
.getString("quote")
server.enqueue(MockResponse()
.setResponseCode(200)
.setBody(response200))
val intent = Intent()
mActivityRule.launchActivity(intent)
onView(withId(R.id.inspirationalQuote))
.check(matches(withText(expectedQuote)))
}
companion object {
val TAG = InspirationalQuoteInstrumentedTest::class.java.simpleName
}
}
And I have this activity:
class InspirationalQuoteActivity : AppCompatActivity() {
private lateinit var quoteService: QuoteOfTheDayService
private var quote: String = ""
private var author: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_inspirational_quote)
val textView = findViewById<TextView>(R.id.inspirationalQuote) as TextView
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
StrictMode.setThreadPolicy(policy)
textView.text = getQuoteOfTheDay()
}
private fun getQuoteOfTheDay(): String {
quoteService = QuoteOfTheDayService()
val qod = quoteService.getQuoteOfTheDay()
val response = qod.execute()
Log.e(TAG, "Response: $response")
response?.let {
quote = response.body()!!.contents.quotes[0].quote
author = response.body()!!.contents.quotes[0].author
}
Log.e(TAG, "Expected Quote: $quote")
return quote
}
companion object {
private val TAG = InspirationalQuoteActivity::class.java.simpleName
}
}
When I run my test getQuoteOfTheDay() gets executed twice. What gives? The issue is that I'm trying to mock out an api call which looks like it's working ask expected, however there is another log that I'm not sure where it's coming from. For reference, here is the out put in logcat
Response: Response{protocol=http/1.1, code=200, message=OK, url=https://quotes.rest/qod}
Expected Quote: Let us think the unthinkable, let us do the undoable, let us prepare to grapple with the ineffable itself, and see if we may not eff it after all.
Response: Response{protocol=http/1.1, code=200, message=OK, url=http://localhost:37290/qod}
Expected Quote: Winning is nice if you don't lose your integrity in the process.
As you can see, I hit https://quotes.rest/qod once, and then I hit my mock server after that.
I missed some arguments in the constructor... Doh.
Changing
ActivityTestRule(InspirationalQuoteActivity::class.java)
to
ActivityTestRule(InspirationalQuoteActivity::class.java, false, false)
did the trick.
You are launching your activity with the intentTestRule
IntentsTestRule<>(InspirationalQuoteActivity.class, false, true);
The third parameter is launchActivity, you must set it as false