Невозможно получить атрибуты для пользовательского представления - PullRequest
0 голосов
/ 12 июля 2020

Я пытаюсь создать приложение, чтобы помочь мастерам подземелий D&D провести кампанию.

Для этого, например, требуется установить очки способностей персонажа, которого игрок использует в кампании.

Учитывая, что любой персонаж в D&D имеет шесть базовых c оценок способностей, я полагал, что кодирование всего на Fragment, хотя и возможно, приведет к беспорядку спагетти-кода, даже если один прибегнет к привязке данных и MVVM паттерны и другие махинации.

Итак, я пошел и попытался создать собственное представление, которое само обрабатывало бы установку одной оценки способности.

Проблема: при попытке для синтаксического анализа атрибутов настраиваемого представления из файла макета XML.

android.view.InflateException: Binary XML file line #19 in com.callisto.kd205e:layout/scores_fragment: Binary XML file line #19 in com.callisto.kd205e:layout/scores_fragment: Error inflating class com.callisto.kd205e.views.scoresetter.ScoreSetterView
    Caused by: android.view.InflateException: Binary XML file line #19 in com.callisto.kd205e:layout/scores_fragment: Error inflating class com.callisto.kd205e.views.scoresetter.ScoreSetterView
    Caused by: java.lang.reflect.InvocationTargetException
        at java.lang.reflect.Constructor.newInstance0(Native Method)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
        at android.view.LayoutInflater.createView(LayoutInflater.java:854)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1006)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:961)
        at android.view.LayoutInflater.rInflate(LayoutInflater.java:1123)
        at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1084)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:682)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:534)
        at androidx.databinding.DataBindingUtil.inflate(DataBindingUtil.java:126)
        at androidx.databinding.DataBindingUtil.inflate(DataBindingUtil.java:95)
        at com.callisto.kd205e.fragments.scores.ScoresFragment.onCreateView(ScoresFragment.kt:44)
        at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2698)
        at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:320)
        at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1187)
        at androidx.fragment.app.FragmentManager.addAddedFragments(FragmentManager.java:2224)
        at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1997)
        at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1953)
        at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1849)
        at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:413)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
     Caused by: kotlin.KotlinNullPointerException
        at com.callisto.kd205e.views.scoresetter.ScoreSetterView.initialize(ScoreSetterView.kt:53)
        at com.callisto.kd205e.views.scoresetter.ScoreSetterView.<init>(ScoreSetterView.kt:26)
        ...

Я попытался создать свое собственное представление, следуя руководству Маттиаса Зигмунда . Возможно, я где-то допустил ошибку, пытаясь реализовать его предложения, но я этого не вижу.

Теперь, для всех вы, отважные души, которые осмеливаются бороться с созданным мною ужасом, вот код:

ScoresFragment.kt:

private const val ARG_PARAM1 = "characterId"

class ScoresFragment : BaseFragment()
{
    private var characterId: Long? = null

    private lateinit var binding: ScoresFragmentBinding

    private lateinit var viewModel: ScoresViewModel

    override fun onCreate(savedInstanceState: Bundle?)
    {
        super.onCreate(savedInstanceState)
        arguments?.let {
            characterId = it.getLong(ARG_PARAM1)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View?
    {
        binding = DataBindingUtil.inflate(
            inflater,
            R.layout.scores_fragment,
            container,
            false
        )

        val application = requireNotNull(this.activity).application

        val database = Kd205eDatabase.getInstance(application).dao

        val viewModelFactory =
            ScoresViewModelFactory(
                database,
                application
            )

        viewModel = ViewModelProvider(this, viewModelFactory)
            .get(ScoresViewModel::class.java)

        binding.scoresViewModel = viewModel

        binding.lifecycleOwner = viewLifecycleOwner

        viewModel.track(characterId!!)

        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?)
    {
        super.onViewCreated(view, savedInstanceState)
    }

    companion object
    {
        @JvmStatic
        fun newInstance(characterId: Long) =
            ScoresFragment().apply {
                arguments = Bundle().apply {
                    putLong(ARG_PARAM1, characterId)
                }
            }
    }
}

scores_fragment. xml (в значительной степени просто тестовый макет):

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:custom="http://schemas.android.com/apk/com.callisto.kd205e"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="scoresViewModel"
            type="com.callisto.kd205e.fragments.scores.ScoresViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".fragments.species.ScoresFragment">

        <com.callisto.kd205e.views.scoresetter.ScoreSetterView
            android:id="@+id/scoreSetterStr"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            custom:attribute_name="Strength"
            custom:attribute_base_score="10"
            custom:attribute_bonus="0"
            custom:attribute_icon_id="@drawable/icons8_strength"
            />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

ScoresViewModel.kt:

class ScoresViewModel
(
    val database: Kd205eDao,
    application: Application
) : BaseViewModel(application)
{
    private var character = MutableLiveData<Character>()

    fun track(characterId: Long)
    {
        uiScope.launch {
            character.value = setUpCharacterFromDB(characterId)
        }
    }

    private suspend fun setUpCharacterFromDB(characterId: Long): Character?
    {
        return withContext(Dispatchers.IO)
        {
            val dbCharacter = database.getSingleCharacter(characterId)!!

            val dbRace = database.getSingleRace(dbCharacter.raceId)

            val attributes = database.checkAllAttributes()

            for (attribute in attributes)
            {
                database.insertCharacterScore(
                    DBAbilityScore(characterId, attribute.attributeId, roll4d6MinusLowest())
                )
            }

            val scores = database.getScoresForCharacter(characterId)

            val result = Character(dbCharacter, dbRace, scores)

            result
        }
    }

    private fun roll4d6MinusLowest(): Int
    {
        val rolls = arrayOf((1..6).random(), (1..6).random(), (1..6).random(), (1..6).random())

        return rolls.sum() - rolls.min()!!
    }
}

ScoreSetterView.kt:

class ScoreSetterView(context: Context, attrs: AttributeSet) :
    CustomConstraintLayout<ScoreSetterState, ScoreSetterModel>(context, attrs)
{
    private var iconId: Int = 0
    private var bonus: Int = 0
    private var baseScore: Int = 0
    private lateinit var attributeName: String

    override lateinit var viewModel: ScoreSetterModel

    init
    {
        initialize(attrs, context)
    }

    private fun initialize(attrs: AttributeSet, context: Context)
    {
        val view = View.inflate(context, R.layout.view_score_setter, this)

        val typedArray = context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.ScoreSetterView,
            0, 0
        )

        try
        {
            // I get the crash right here
            attributeName = typedArray.getString(R.styleable.ScoreSetterView_attribute_name)!!
            baseScore = typedArray.getInteger(R.styleable.ScoreSetterView_attribute_base_score, 0)
            bonus = typedArray.getInteger(R.styleable.ScoreSetterView_attribute_bonus, 0)
            iconId = typedArray.getInteger(R.styleable.ScoreSetterView_attribute_icon_id, 0)

            txtBaseScore.text = baseScore.toString()
            txtBonus.text = bonus.toString()
            txtFinalScore.text = (baseScore + bonus).toString()
            imgAttributeIcon.setImageDrawable(resources.getDrawable(iconId, null))

            viewModel = ScoreSetterModel()

            viewModel.attributeModifier.value = bonus
            viewModel.attributeScore.value = baseScore
            viewModel.attributeName.value = attributeName
        }
        finally
        {
            typedArray.recycle()
        }
    }

    override fun onLifecycleOwnerAttached(lifecycleOwner: LifecycleOwner)
    {
        observeLiveData(lifecycleOwner)
    }

    private fun observeLiveData(lifecycleOwner: LifecycleOwner)
    {
        viewModel.getLabel().observe(lifecycleOwner, Observer {

        })

        viewModel.getScore().observe(lifecycleOwner, Observer {
            txtFinalScore.text = viewModel.getFinalScore().toString()
        })

        viewModel.getModifier().observe(lifecycleOwner, Observer {
            txtFinalScore.text = viewModel.getFinalScore().toString()
        })
    }
}

ScoreSetterModel.kt:

class ScoreSetterModel: CustomViewModel<ScoreSetterState>
{
    var attributeName = MutableLiveData<String?>()

    var attributeScore = MutableLiveData<Int?>()

    var attributeModifier = MutableLiveData<Int?>()

    var attributeIcon = MutableLiveData<String?>()

    override var state: ScoreSetterState? = null
        get() = ScoreSetterState(
            attributeName.value,
            attributeScore.value,
            attributeModifier.value,
            attributeIcon.value
        )
        set(value) {
            field = value
            restore(value)
        }

    fun getLabel(): LiveData<String?> = attributeName

    fun getScore(): LiveData<Int?> = attributeScore

    fun getModifier(): LiveData<Int?> = attributeModifier

    fun getIcon(): LiveData<String?> = attributeIcon

    fun getFinalScore(): Int {
        return getScore().value!! + getModifier().value!!
    }

    private fun restore(state: ScoreSetterState?)
    {
        attributeName.value = state?.attributeName
        attributeScore.value = state?.attributeValue
        attributeModifier.value = state?.attributeModifier
        attributeIcon.value = state?.attributeIcon
    }
}

ScoreSetterState.kt:

@Parcelize
data class ScoreSetterState(
    val attributeName: String?,
    val attributeValue: Int?,
    val attributeModifier: Int?,
    val attributeIcon: String?
): CustomViewState

CustomConstraintLayout.kt:

abstract class CustomConstraintLayout<V: CustomViewState, T: CustomViewModel<V>>
    : ConstraintLayout, CustomView<V, T>
{
    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)

    constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)

    override fun onAttachedToWindow()
    {
        super.onAttachedToWindow()

        val lifeCycleOwner = context as? LifecycleOwner ?: throw LifecycleOwnerNotFoundException()

        onLifecycleOwnerAttached(lifeCycleOwner)
    }

    override fun onSaveInstanceState() = CustomViewStateWrapper(super.onSaveInstanceState(), viewModel.state)

    @Suppress("UNCHECKED_CAST")
    override fun onRestoreInstanceState(state: Parcelable?)
    {
        if (state is CustomViewStateWrapper)
        {
            viewModel.state = state.state as V

            super.onRestoreInstanceState(state.superState)
        }
    }
}

CustomView.kt:

interface CustomView<V: CustomViewState, T: CustomViewModel<V>>
{
    val viewModel: T

    fun onLifecycleOwnerAttached(lifecycleOwner: LifecycleOwner)
}

CustomViewState.kt:

interface CustomViewState: Parcelable

CustomViewModel.kt:

interface CustomViewModel<T: CustomViewState>
{
    var state: T?
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...