Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Moshi's Custom Adapter with RxAndroid & Retrofit & Kotlin

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.

like image 260
iForests Avatar asked Nov 04 '16 16:11

iForests


2 Answers

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)
    }
}
like image 190
Praveer Gupta Avatar answered Oct 29 '22 11:10

Praveer Gupta


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

interface AccountService {

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

No need for any other custom adapter.

like image 37
Serj Lotutovici Avatar answered Oct 29 '22 13:10

Serj Lotutovici