LaVOZs

The World’s Largest Online Community for Developers

'; android - Moshi's Custom Adapter with RxAndroid & Retrofit & Kotlin - LavOzs.Com

After configuring Kotlin for Android project, I wrote a simple MainActivity.kt. It called Retrofit to get a JSON file which contained the following data:

{
    "message": "success",
    "user": {
        "username": "Eric"
    }
}

Now I want to use Moshi to convert the JSON data to Kotlin's class, so here are the two classes to reflect the above JSON structure:

class User(var username: String)

class UserJson(var message: String, var user: User)

And a custom type adapter for Moshi:

class UserAdapter {
    @FromJson fun fromJson(userJson: UserJson) : User {
        Log.d("MyLog", "message = ${userJson.message}")  // = success
        Log.d("MyLog", "user = ${userJson.user}")        // = null

        return userJson.user
    }
}

When it goes into the function fromJson(), userJson.message = "success" as expected. But the strange thing is that userJson.user is null, which should be User(username="Eric").

I am new to Moshi and Kotlin, and I have already stuck with this problem for about 10 hours. Please help me out. Thanks for any help.

========================================

The following is the entire code of MainActivity.kt (50 lines only):

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Custom Type Adapters for Moshi
        val userMoshi = Moshi.Builder().add(UserAdapter()).build()

        val retrofit = Retrofit.Builder()
                .baseUrl("https://dl.dropboxusercontent.com/")
                .addConverterFactory(MoshiConverterFactory.create(userMoshi))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build()

        val accountService = retrofit.create(AccountService::class.java)

        accountService.signUpAnonymously()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe { user ->
                    Log.d("MyLog", user.toString())
                }
    }
}


// ========== For Retrofit ==========
interface AccountService {

    @GET("u/17350105/test.json")
    fun signUpAnonymously() : Observable<User>

}


// ========== For Moshi ==========
class User(var username: String)

class UserJson(var message: String, var user: User)

class UserAdapter {

    @FromJson fun fromJson(userJson: UserJson) : User {
        Log.d("MyLog", "message = ${userJson.message}")        // = success
        Log.d("MyLog", "user = ${userJson.user}")              // = null

        return userJson.user
    }

}

The build.gradle is:

compile "io.reactivex.rxjava2:rxjava:2.0.0"
compile "io.reactivex.rxjava2:rxandroid:2.0.0"

compile "com.android.support:appcompat-v7:25.0.0"

compile "com.squareup.retrofit2:retrofit:2.1.0"
compile "com.squareup.retrofit2:converter-moshi:2.1.0"
compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'

Thank you again.

You can solve the problem by changing your code to do something like below.

Basically in your case when the UserAdapter is registered, it tells moshi that it can create a User only from UserJson object. Hence Moshi does not recognize the JSON object with keyword user.

By adding an indirection in form of User1 (please pardon the naming convention), the UserJson is created properly with User1 from JSON.

class User(var username: String)

class User1(var username: String) // I introduced this class
class UserJson(var message: String, var user: User1) // changed User to User1

class UserAdapter {
    @FromJson fun fromJson(userJson: UserJson): User {
        println("message = ${userJson.message}")        
        println("user = ${userJson.user}")              
        return User(userJson.user.username)
    }
}

If you just need the User object. There is a library called Moshi-Lazy-Adapters that provides a @Wrapped annotation, that allows to specify the path to the desired obejct. All you have to do is add the respective adapter to your moshi instance and chagne the service code to:

interface AccountService {

  @GET("u/17350105/test.json")
  @Wrapped("user")
  fun signUpAnonymously() : Observable<User>
}

No need for any other custom adapter.

Related
Kotlin Ternary Conditional Operator
Logging with Retrofit 2
What is the equivalent of Java static methods in Kotlin?
Val and Var in Kotlin
how to get response in retrofit while using rxAndroid?
mapping custom data RxAndroid with Kotlin