Gson невозможно вызвать конструктор без аргументов для наблюдателя ( LiveData) в моем проекте Android и Kotlin

#android #kotlin #gson #android-room #android-livedata

#Android #kotlin #gson #android-комната #android-livedata

Вопрос:

Я уже прочитал много статей о переполнении стека и других сообщений в блогах об этом, пробуя различные решения для похожих (но не одинаковых) проблем, которые у них там были.

Я структурирую этот пост следующим образом:

  1. Моя проблема
  2. Мой код (та часть, которую я считаю актуальной)
  3. Что я пытался это исправить

1. Моя проблема

Я получаю следующее сообщение об ошибке:

     Process: com.myapp, PID: 23553
    java.lang.RuntimeException: Unable to invoke no-args constructor for androidx.arch.core.internal.SafeIterableMap$SupportRemove<androidx.lifecycle.Observer<? super java.lang.Integer>, androidx.lifecycle.LiveData$ObserverWrapper>. Registering an InstanceCreator with Gson for this type may fix this problem.
        at com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:228)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:212)
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.read(TypeAdapterRuntimeTypeWrapper.java:41)
        at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:186)
        at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:145)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:131)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:222)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:131)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:222)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:131)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:222)
        at com.google.gson.Gson.fromJson(Gson.java:932)
        at com.google.gson.Gson.fromJson(Gson.java:897)
        at com.google.gson.Gson.fromJson(Gson.java:846)
        at com.myapp.ui.customGame.CustomGameViewModel.initialize(CustomGameViewModel.kt:46)
  

Описание того, чего я пытаюсь достичь:

Я пишу приложение как проект для изучения программирования на Kotlin и создания приложений в целом, я относительно новичок в создании приложений и Kotlin, имейте это в виду, если вы видите, что я делаю глупые ошибки, пожалуйста ;). В моем приложении у меня есть действие, содержащее фрагмент, который позволяет вам выбирать настройки для игры в волейбол (CustomGameSetupFragment). Настройки включают в себя простые вещи, такие как итоговый счет, в котором выигрывает команда, Имена и т. Д. После выбора и сохранения настроек приложение создает объект класса Game с примененными настройками и сохраняет их в базе данных комнат. Объект в таблице моей базы данных в основном содержит идентификатор, некоторую другую информацию и строку JSON игрового объекта (созданную с помощью пакета Google Gson). Затем действие заменяет фрагмент фрагментом, который позволяет подсчитывать количество очков в игре и просматривать имена и прочее (пользовательский фрагмент). Новый фрагмент создает объект ViewModel, который затем снова считывает игры из базы данных, выбирает последние сохраненные объекты, а затем пытается воссоздать игровой объект из сохраненной строки JSON. Это делается путем выполнения:

 val gameType = object: TypeToken<Game>() {}.type
this.game = Gson().fromJson<Game>(
    gameRepo.ongoingGameData[0].gameJson,
    gameType
    //Game::class.java // this was an other method I tried. Also didnt work
)
  

Раньше класс Game не содержал LiveData / MutableLiveData, но это привело к необходимости приведения атрибутов к LiveData / MutableLiveData в классе ViewModel, и это привело к множеству ошибок. Но это сработало!. Я реорганизовал класс Game, чтобы он в основном использовал атрибуты LiveData / MutableLiveData (те, которые мне нужны, чтобы быть LiveData), поскольку в CustomGameFragment и его ViewModel это позволило бы мне просто напрямую наблюдать за атрибутами игры. Но после того, как я реорганизовал класс, Gson больше не может его загружать.
Я не уверен, что это просто потому, что я использую LiveData, и им каким-то образом нужен контекст или viewLifeCylceOwner, которые они получают неявно в ViewModel или что-то в этом роде.

2. My Code

a) The Room database (with Repository, Entity, Dao, Database)

Entity:

 package com.myapp.data

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "gameData_table")
data class GameData(
    @PrimaryKey(autoGenerate = true) val id: Int?,
    @ColumnInfo(name = "gid") val gid: String?,

    @ColumnInfo(name = "is_over") val isOver : Boolean?,

    @ColumnInfo(name = "team1_name") val team1Name: String?,
    @ColumnInfo(name = "team2_name") val team2Name: String?,

    @ColumnInfo(name = "team1_sets") val team1Sets: Int?,
    @ColumnInfo(name = "team2_sets") val team2Sets: Int?,

    @ColumnInfo(name = "total_sets") val totalSets: Int?,
    @ColumnInfo(name = "game_json") val gameJson : String?

    )
  

The Dao:

 package com.myapp.data

import androidx.room.*

@Dao
interface GameDao {
    @Query("SELECT * FROM gameData_table")
    suspend fun getAll(): List<GameData>

    @Query("SELECT * FROM gameData_table WHERE id = (:id)")
    fun loadAllByIds(id: Array<Int>): List<GameData>

    @Query("SELECT * FROM gameData_table WHERE is_over = 0")
    suspend fun getOngoing() : List<GameData>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(vararg game: GameData)

    @Delete
    suspend fun delete(game: GameData)

    @Query("DELETE FROM gameData_table WHERE is_over = 0")
    suspend fun deleteOngoing()

    @Query("UPDATE gameData_table SET game_json = (:json) WHERE gid = (:gameId)")
    suspend fun updateJSON(json: String, gameId : String)
}
  

The Database:

 package com.myapp.data

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [GameData::class], version = 1, exportSchema = false)
abstract class GameDatabase : RoomDatabase() {
    abstract fun gameDao() : GameDao

    companion object {
        //Singleton pattern to prevent multiple instances of the database

        @Volatile
        private var INSTANCE: GameDatabase? = null

        fun getDatabase(context: Context) : GameDatabase {
            return INSTANCE ?: synchronized(this){
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    GameDatabase::class.java,
                    "game_database"
                ).build()

                INSTANCE = instance
                return instance
            }
        }
    }
  

The Repository:

 package com.myapp.data

import kotlinx.coroutines.runBlocking

class GameRepository (private val gameDao: GameDao){
    val allGameData: List<GameData> = runBlocking { gameDao.getAll()}
    val ongoingGameData : List<GameData> = runBlocking { gameDao.getOngoing() }

    suspend fun insert(gameData : GameData){
        gameDao.insertAll(gameData)
    }

    suspend fun deleteOngoing() {
        gameDao.deleteOngoing()
    }

    suspend fun updateGame(gameData : GameData){
        gameDao.updateJSON(gameData.gameJson!!, gameData.gid!!)
    }

}
  

b) The Game class

А теперь очень короткая версия игры, поскольку большинство методов на самом деле не имеют отношения к моей проблеме, я думаю:

 package com.myapp.game

import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.myapp.data.GameData
import com.myapp.values.Values
import com.google.gson.Gson

class Game {


    /*
    No live data needed or possible?
     */
    private var sets : MutableList<Set>
    private val pointGap : Int = Values.DEFAULT_POINT_GAP
    private val gid : String = this.toString()

    /*
    Live data needed or possible
     */

    // private MutableLiveData
    private var _team1Name : MutableLiveData<String> = MutableLiveData(Values.DEFAULT_TEAM1_NAME)
    (more strings ...)
    private var _setWinScore : MutableLiveData<Int> = MutableLiveData(Values.DEFAULT_WIN_SCORE)
    (...)

    // public LiveData
    val team1Name : LiveData<String>
    (more strings ...)
    val setWinScore : LiveData<Int>
    (...)


    init{
        team1Name = _team1Name
        (more strings ...)
        setWinScore = _setWinScore
        (...)

    }


    constructor(gameSettings: GameSettings = GameSettings()){
        this._team1Name.value = gameSettings.team1Name
        (more strings...)
        this._setWinScore.value = gameSettings.setWinScore
        (...)
    }
}
  

3. Подходы к его исправлению#

Я пытался использовать InstanceCreator. Но после того, как я прочитал кое-что об этом, я обнаружил, что это необходимо, если объект, который вы хотите воссоздать, имеет аргумент чего-то, что класс Gson должен знать, чтобы поместить его (например, контекст). Я думаю, у меня этого нет (?).
Я все равно попробовал, что, конечно, не сработало. Также я попробовал несколько вариантов использования TypeToken, которые я также показал в начале.

Еще одна вещь, которую я часто читал, заключалась в использовании новейшей версии пакета Gson, Room и LiveData или использовании kapt вместо ключевых слов реализации в grandle.build на уровне проекта. Я попробовал оба -> одно и то же исключение

Итак, у вас есть какие-нибудь идеи?

Или я делаю что-то глупо неправильно?
Заранее спасибо, что пожертвовали своим временем!

PS: Я не являюсь носителем английского языка, поэтому прошу прощения за плохую грамматику и орфографию.

Ответ №1:

Ниже показано, как выполнить десериализацию LiveData , однако, возможно, в вашем случае было бы более целесообразно поделиться Game данными как ViewModel ? См. Страницу разработчиков Android.


Когда не соответствует ни один пользовательский или встроенный адаптер типа, Gson использует основанный на отражении. Проблема в том, что вы просите Gson десериализовать JSON как LiveData . Если вы посмотрите на исходный код LiveData , вы увидите, что он имеет несколько закрытых полей, и для типа одного из них Gson не может создавать экземпляры.

В общем, не рекомендуется использовать сериализацию или десериализацию на основе отражения Gson для любых сторонних классов (здесь LiveData ), потому что тогда вы полагаетесь на их внутренние детали реализации, которые могут измениться в любой момент.

Это можно решить, создав пользовательский TypeAdapterFactory .
Я не знаком с Kotlin, но, надеюсь, следующий Java-код, тем не менее, будет полезен для вас в качестве примера:

 class LiveDataTypeAdapterFactory implements TypeAdapterFactory {
  public static final LiveDataTypeAdapterFactory INSTANCE = new LiveDataTypeAdapterFactory();
  
  private LiveDataTypeAdapterFactory() { }
  
  @Override
  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    Class<?> rawType = type.getRawType();
    // Only handle LiveData and MutableLiveData
    if (rawType != LiveData.class amp;amp; rawType != MutableLiveData.class) {
      return null;
    }
    
    // Assumes that LiveData is never used as raw type but is always parameterized
    Type valueType = ((ParameterizedType) type.getType()).getActualTypeArguments()[0];
    // Get the adapter for the LiveData value type `T`
    // Cast TypeAdapter to simplify adapter code below
    @SuppressWarnings("unchecked")
    TypeAdapter<Object> valueAdapter = (TypeAdapter<Object>) gson.getAdapter(TypeToken.get(valueType));
    
    // Is safe due to `type` check at start of method
    @SuppressWarnings("unchecked")
    TypeAdapter<T> adapter = (TypeAdapter<T>) new TypeAdapter<LiveData<?>>() {
      @Override
      public void write(JsonWriter out, LiveData<?> liveData) throws IOException {
        // Directly write out LiveData value
        valueAdapter.write(out, liveData.getValue());
      }
      
      @Override
      public LiveData<?> read(JsonReader in) throws IOException {
        Object value = valueAdapter.read(in);
        return new MutableLiveData<>(value);
      }
    };
    return adapter;
  }
}
  

(Обратите внимание, что при этом не сохраняются наблюдатели LiveData )

Затем вы можете создать Gson экземпляр с помощью a GsonBuilder и зарегистрировать фабрику:

 Gson gson = new GsonBuilder()
  .registerTypeAdapterFactory(LiveDataTypeAdapterFactory.INSTANCE)
  .create();
  

Нет необходимости использовать TypeToken при десериализации Game , прямое использование класса также будет работать. TypeToken предназначен для универсальных типов.

В идеале вы также должны создать TypeAdapterFactory for MutableList , чтобы не полагаться на его внутреннюю реализацию.