I am really confused and I need your help! This is only my second App and my first time to work with REST API's. I am simply trying to display some User Information like name and profile picture. It is working perfectly fine when I am using the code in the main Activity, but as soon as I am using a different class for it the API call fails and the code is pretty similar, so I do not know any further. Since Twitter uses Retrofit in their own tutorial I am using it as well.
My Class extending TwitterApiClient, the file is also including the Interface for the custom service:
import android.util.Log
import com.twitter.sdk.android.core.*
import com.twitter.sdk.android.core.models.User
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
class MyTwitterApiClient(session: TwitterSession) : TwitterApiClient(session) {
fun getCustomService() : GetUsersShowAPICustomService {
return getService(GetUsersShowAPICustomService::class.java)
}
}
interface GetUsersShowAPICustomService {
#GET("/1.1/users/show.json")
fun show(#Query("user_id") userId: Long) : Call<User>
}
My Method in the MainActivity looks like this:
private fun loadTwitterAPI(userID: Long) {
MyTwitterApiClient(session).getCustomService().show(userID).enqueue(object : Callback<User>() {
override fun success(result: Result<User>?) {
text.text = (
"Name: "+result!!.data.name
+"\nLocation: "+result!!.data.location
+"\nFriends: "+result!!.data.friendsCount
)
Picasso.with(baseContext).load(result!!.data.profileImageUrl).resize(250, 250).into(imageView)
}
override fun failure(exception: TwitterException?) {
}
})
}
This works perfectly fine, but I do not want to have the call itself in my main activity and I created a companion Object in my Class extending the TwitterApi which should simply get called with the TwitterSession as parameter and it should return an object of the class User which contains all the important data.
The companion Object inside the MyTwitterApiClient class looks like this:
companion object {
fun start(session: TwitterSession): User {
val userID = session.userId
var data: User? = null
MyTwitterApiClient(session).getCustomService().show(userID).enqueue(object : Callback<User>() {
override fun success(result: Result<User>?) {
data = result!!.data
}
override fun failure(exception: TwitterException?) {
throw exception!!
}
})
return data!!
}
}
The new Method in the MainActivity looks like this:
private fun loadTwitterAPI(userID: Long) {
val t = MyTwitterApiClient.start(session)
text.text = (
"Name: "+t.name
+"\nLocation: "+t.location
+"\nFriends: "+t.friendsCount
)
Picasso.with(baseContext).load(t.profileImageUrl).resize(250, 250).into(imageView)
}
Through testing, I found out, that neither the success Method nor the failure Method gets called. And I do not understand at all why it does not call any Method and just fails.
If anyone here already worked with something like this or has a Tip for me it would be super helpful!
Greetings
Btw: The error that crashes my app in the end is a NullPointerException as the Success Method is not called and null gets returned in the end.
Pastebin to my files:
MainActivity: https://pastebin.com/hWByYUFT
MyTwitterApiClient: https://pastebin.com/85xH284K
activity_main.xml: https://pastebin.com/vkzbkL81
depencies in build.gradle: https://pastebin.com/CpX7cwkS
Ok, starting from your code:
fun start(session: TwitterSession): User {
val userID = session.userId
var data: User? = null
MyTwitterApiClient(session).getCustomService().show(userID).enqueue(object : Callback<User>() {
override fun success(result: Result<User>?) {
data = result!!.data
}
override fun failure(exception: TwitterException?) {
throw exception!!
}
})
return data!!
}
Here you are returning data as if it was assigned. You TwitterApiClient does asynchronous task and so the data from data = result!!.data wont be read correctly from
text.text = (
"Name: "+t.name
+"\nLocation: "+t.location
+"\nFriends: "+t.friendsCount
)
Because t is null then. Its data is not yet set. It will be, sometime in the futur, in the asynchronous callback success().
Your main issue seems to be with how to work with asynchronous tasks and how to notify results. Plenty of sources about it. LiveData, RxJava, EventBus might all be leads.
BTW, the reason why your code worked in MainActivity was because you were setting the text after the result came (in success()), so t was good to read.
Good luck and happy learning!
Related
I have the following code which i think is valid, because the recursion happens as a result of a callback. It's not called directly as a result of the function call. But the compiler seems to think there is a recursion issue
class Model(callBack: CallBack) {
interface CallBack {
fun onSomething()
}
}
class SomeClass {
fun createModel() = Model(callBack)
val callBack = object : Model.CallBack {
override fun onSomething() {
val anotherModel = createModel()
// Use model for something
}
}
}
Type checking has run into a recursive problem. Easiest workaround: specify types of your declarations explicitly
Is there a workaround for this?
EDIT
I also tried changing callBack to a function so that the same instance is not referenced by multiple models, but I get the same error
The recursive problem mentioned is not about function calls, it's about the compiler trying to find out the types of the declaration and it has stuck in a recursive type checking. It wants to find the output type of createModel which depends on the type of val callback and it depends on createModel again. As it says, declare their types to fix the issue.
class Model(callBack: CallBack)
{
interface CallBack {
fun onSomething()
}
}
class SomeClass {
fun createModel() : Model = Model(callBack)
val callBack : Model.CallBack = object : Model.CallBack {
override fun onSomething() {
val anotherModel : Model = createModel()
// Use model for something
}
}
}
I want to check if my database write was successful in order to show the user an error message.
My current approach doesn't work as it says "Type mismatch, required Unit found EmailStatus"
Current approach
class EmailRepositoryImpl : EmailRepository {
private val db = Firebase.firestore
override fun sendEmail(email: Email): EmailStatus<Nothing> {
db.collection("emails").document().set(email).addOnCompleteListener {
if (it.isSuccessful) return#addOnCompleteListener EmailStatus.Success<Nothing>
if (it.isCanceled) return#addOnCompleteListener EmailStatus.Error(it.exception!!)
}
}
}
Status Sealed Class
sealed class EmailStatus<out T> {
data class Success<out T>(val data: T) : EmailStatus<T>()
data class Error(val exception: Exception) : EmailStatus<Nothing>()
}
Is it even possible to write something like this? As far as I know there is a generic firebase error type but I didn't found anything related to kotlin or android...
I appreciate every help, thank you
Edit
I've tried getting my document, but I am just getting null: (When I use the listener approach, everything works fine)
Interface
interface EmailRepository {
suspend fun getEmail(): Flow<EmailEntity?>
}
Interface Implementation
override suspend fun getEmail(): Flow<EmailEntity?> = flow {
val result = db.collection("emailprice").document("Email").get().await()
emit(result.toObject<EmailEntity>())
}
ViewModel
private val emailEntity = liveData<EmailEntity?>(Dispatchers.IO) {
emailRepository.getCalibratePrice()
}
The problem is that addOnCompleteListener callback does not return anything (Unit) and you are trying to return an EmailStatus from that scope.
You have three approaches:
Create an interface that will populate the value and return that EmailStatus down to your caller layer
Use Coroutines to suspend this function when the async call to firebase is done and then return that value
Use Flow to offer the data when it's ready to process
I think the easiest way to do this one shot operation is to use Coroutines; I have written an article about that.
Okay, this is the final solution, thanks to #Gastón Saillén and #Doug Stevenson :
EmailRepository
interface EmailRepository {
fun sendEmail(email: Email): Flow<EmailStatus<Unit>>
}
EmailRepository Implementation
class EmailRepositoryImpl #Inject constructor(
private val db: FirebaseFirestore
) : EmailRepository {
override fun sendEmail(email: Email)= flow<EmailStatus<Unit>> {
db.collection("emails").add(email).await()
emit(EmailStatus.success(Unit))
}.catch {
emit(EmailStatus.failed(it.message.toString()))
}.flowOn(Dispatchers.Main)
}
ViewModel
fun sendEmail(): LiveData<EmailStatus<Unit>> {
val newEmail = createEmail()
return emailRepository.sendEmail(newEmail).asLiveData()
}
Fragment
btn.setOnClickListener {
viewModel.sendEmail().observe(viewLifecycleOwner) {
when(it) {
is EmailStatus.Success -> {
valid = true
navigateTo(next, bundleNext)
Toast.makeText(requireContext(), "Success", Toast.LENGTH_SHORT).show()
}
is EmailStatus.Failure -> {
valid = false
Toast.makeText(requireContext(), "Failed ${it.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
The only problem I currently have is that my "faield state" does not work like it should.
It should fail, if the user has no internet access. Currently, the write to the db never fails and Firebase just waits till the user has internet access. The problem here is that when I click multiple times, the write is executed multiple times. But I think I have to implement a bit more logic here and the above written code is fine like it currently is.
I have certain memory leaks happening in my custom handler class ,but not sure how to fix it. checkedout a couple of examples online but nothing is specific to my code so not sure how to go about it :
private val startupCallback = object: RetryCallback(NUMBER, DELAY) {
override fun onRetry(retryCount: Int) {
mySdkApi.applicationStartup(this)
}
override fun onCompleted(): Boolean {
updateStatus(Callback.Status.StartUpSDK)
return true
}
override fun onFailed(e: MyException?) {
updateStatus(Callback.Status.StartUpSDK, "", e)
}
}
Android studio keeps prompting "This handler class should be static or leaks might occur".Any ideas how to go about it?
The Android Studio complaining is pretty reasonable. The problem is that anonymous classes capture reference to the parent class that they were created in.
There are basically two solutions the "not pretty" and the ugly.) Both of them are about WeakReference.
#1 The not pretty solution is to make a class that will take a weak ref
class ApiRetryCallback(activity: Activity): RetryCallback(NUMBER, DELAY) {
private val weakActivity = WeakReference(activity)
override fun onRetry(retryCount: Int) {
weakActivity.get()!!.mySdkApi.applicationStartup(this) //or weakThis.get()? to swallow null cases
}
override fun onCompleted(): Boolean {
weakActivity.get()!!.updateStatus(Callback.Status.StartUpSDK)
return true
}
override fun onFailed(e: MyException?) {
weakActivity.get()!!.updateStatus(Callback.Status.StartUpSDK, "", e)
}
}
In activity:
private val startupCallback = ApiRetryCallback(this) //this is MainActivity here
#2 The ugly solution is based on a fact that lambdas should capture parent reference, only where there is a direct usage of it. So I came up with this substitution and I didn't see strong references in a debugger but you should check that:
private val startupCallback = {
val weakActivity = WeakReference(this#MainActivity)
object : RetryCallback(NUMBER, DELAY) { //returned as last expression
override fun onRetry(retryCount: Int) {
weakActivity.get()!!.mySdkApi.applicationStartup(this) //or weakThis.get()? to swallow null cases
}
//....else methods....
}
}()
Here the lambda will be called immediately and will capture only the weak reference inside the object, also it will return the last expression wich is object.
#3 While I was writing, I came up with a third solution, which is close to #2
private val startupCallback = WeakReference(this).let { //this here is MainActivity
val weakActivity = it //it of let scope wich is WeakReference
object : RetryCallback(NUMBER, DELAY) { //returned as last expression
override fun onRetry(retryCount: Int) {
weakActivity.get()!!.mySdkApi.applicationStartup(this) //or weakThis.get()? to swallow null cases
}
//....else methods....
}
}
Anonymous classes (like yours) are non static. You can replace anonymous class with the normal class (just create class extending RetryCallback) and pass all needed objects as constructor arguments.
I'm a newbie in Kotlin.
I'm building an app like Twitter.
I want to create custom class extends TwitterApiClient - to use more endpoints. Twitter's tutorial is here
Tutorial
Here's my code:
class TwitterApiList(session: TwitterSession) : TwitterApiClient(session) {
fun getHomeTimeline(): TwitterCustom {
return getService(TwitterCustom::class.java)
}
}
// TwitterCustom interface
public interface TwitterCustom {
#GET("/1.1/statuses/home_timeline.json")
fun home_timeline(#Query("count") count: Int?, #Query("since_id") since_id: Int?, #Query("max_id") max_id: Int?, cb: Callback<List<Tweet>>)
}
// And how I use it
val apiClient = TwitterApiList(TwitterCore.getInstance().sessionManager.activeSession)
apiClient.getHomeTimeline().home_timeline(null, null, null, object : Callback<List<Tweet>>() {
override fun success(result: Result<List<Tweet>>?) {
Log.d("result", result.toString())
}
override fun failure(exception: TwitterException?) {
Log.d("failed", exception?.message)
}
})
When I run app, it’s always crash with message “Service methods cannot return void.” at this line
apiClient.getHomeTimeline().home_timeline(null, null, null, object : Callback<List<Tweet>>()
Please help me to solve this problem.
Thank you all.
Your callback is calling methods that return Unit. I.e. success() has no return value, Log.d returns Unit, so the Unit is implied. Same goes for failure().
Look closer at the Callback class. If you have and IDE with "Intelli-sense" or the like, it will help you. Otherwise, look at the interface/class and determine how the methods are defined, specifically, look at the return type and either change your calls to match or change the Callback<T> methods to be Unit (assuming it is your interface). This link can help you figure out how to write your functions to return specific types.
Let's say you find something like this...
interface Callback<T> {
fun success(result: Result<T>) : T //notice the : T determines the type of the function
//... other members
}
Then your code might look like this...
apiClient.getHomeTimeline().home_timeline(null, null, null, object : Callback<List<Tweet>>() {
override fun success(result: Result<List<Tweet>>?) {
Log.d("result", result.toString())
return result.getMemberForT() // I made up the name, would need
// decl. for Result<T>
}
//...
}
But without the declarations for Callback and Result, I would only be guessing.
I'm new to writing tests and using Mockito.
I've read the similar topics here on Stackoverflow and made the suggested changes, making sure that regarded classes / interfaces / methods are open.
I tried to follow this
Mocking the constructor injected dependencies
This is the test I came up with so far
class RegistrationPresenterTest {
#Test
fun testRegisterSuccess() {
val mockService = mock<IHerokuInteractor>()
val mockLocal = mock<ILocalStorageInteractor>()
val mockView = mock<RegisterView>()
val mockRegistrationResponse = HerokuRegisterResponse("hash")
val mockPair = ImeiPair("imei","hash")
val presenter = RegisterPresenterImpl(mockLocal,mockService)
whenever(mockService.register(any())).thenReturn(Observable.just(mockRegistrationResponse))
whenever(mockLocal.clearPreferences()).thenReturn(Observable.just(true))
whenever(mockLocal.putImeiPair(any())).thenReturn(Observable.just(true))
//whenever(presenter.writeImeiPairLocally(any())) How do I specify parameters since it uses a parameter from the register method?
presenter.bindView(mockView)
presenter.register("imei","male")
verify(mockService, times(1)).register(any())
verify(mockLocal,times(1)).clearPreferences()
verify(mockLocal,times(1)).putImeiPair(any())
verify(mockView,times(1)).moveToMain()
}
but the response I keep getting is
Wanted but not invoked:
registerPresenterImpl.writeImeiPairLocally(
<any com.company.appname.model.ImeiPair>
);
Actually, there were zero interactions with this mock.
I got this response even when I don't mention that method in the test.
This is my presenter register method. I've changed the classes / interfaces & methods involved to open (kotlin). I believe override methods are open by nature in kotlin.
open class RegisterPresenterImpl #Inject constructor(val localStorage : ILocalStorageInteractor, var herokuService : IHerokuInteractor)
override fun register(imei : String, gender : String){
subscription = herokuService.register(RegisterObject(imei,gender)).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribe(
{
registrationResult ->
Log.d(TAG,"${registrationResult}")
if(registrationResult.imei_hash != null){
writeImeiPairLocally(ImeiPair(imei,registrationResult.imei_hash))
}
else{
Log.e(TAG,"User already exists")
}
},
{
errorResponse -> Log.e(TAG,"Could not register user ${errorResponse.message}")
}
)
addSubscription(subscription)
}
and similarly the
open fun writeImeiPairLocally(pair : ImeiPair){
subscription = localStorage.clearPreferences().flatMap {
cleared -> localStorage.putImeiPair(pair)}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribe(
{
booleanResult -> view?.moveToMain()
},
{
errorResponse -> Log.e(TAG,"Could not write ImeiPair to SharedPreferences ${errorResponse.message}")
}
)
addSubscription(subscription)
}
Here is interfaces
open interface ILocalStorageInteractor : ILocalStorage{
fun getImeiPair() : Observable<ImeiPair>
fun putImeiPair(pair: ImeiPair) : Observable<Boolean>
}
open interface ILocalStorage {
fun clearPreferences() : Observable<Boolean>
}
All help is appreciated.
If you are using plain jUnit, then your AndroidSchedulers.mainThread() is null. That's why onNext is not called.
You need to override Schedulers in a setUp() method with:
RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
#Override
public Scheduler getMainThreadScheduler() {
return Schedulers.immediate(); // or .test()
}
});
To avoid concurrency in tests, I would recommend to override Schedulers.io() like this:
RxJavaHooks.setOnIOScheduler(scheduler1 -> Schedulers.immediate());
If you are going to use TestScheduler, don't forget to call TestScheduler.triggerActions() method.
Also don't forget to unregister Schedulers in tearDown() like this:
RxJavaHooks.reset();
RxAndroidPlugins.getInstance().reset();
AndroidSchedulers.reset();
Schedulers.reset();