Welcome to half 4 of the “Constructing language studying apps with Compose” collection. This can be a collection by which I’ll share my means of constructing a language studying app.
On this collection, I will share my progress, design decisions, and any new insights I gained alongside the way in which. At present I’ll cowl the times 13–16.
In case you have not learn the primary three articles, you could find them right here:
If you wish to know precisely what code modifications I made, you’ll be able to click on [Code Diff] hyperlink subsequent to the date title and see all modifications for that date.
It took a very long time to jot down these, any more I’ll proceed to explain the whole lot I’ve finished however will solely clarify a number of issues. If you wish to perceive how I did one thing I did not clarify, you’ll be able to test it out [Code Diff].
I began day 13 by modifying the deck in order that when pressed it navigates to the apply part for the respective deck.
I’ve additionally added symbols to the deck to point out what number of playing cards you need to overview (left) and what number of playing cards you have discovered (proper).
Not a giant change however fairly helpful info for customers.
On the 14th, I eliminated all of the pretend decks I had and created 2 decks utilizing actual phrases.”20 Most Widespread Italian Nouns” and “Phrases for Italian Vacationers”. Sooner or later there will likely be a whole lot of decks however for now these 2 decks will at the very least make the apply look extra real looking.
I additionally added some code to point out Toast if the person hits a deck that has no playing cards to contemplate. To try this, I added a brand new motion to MyDeckListAction
.
sealed interface MyDeckListAction {
...
object ShowNoCardsToReviewInfo : MyDeckListAction
}
Then within the navigation to apply technique, I examine to see if there are any phrases that must be reviewed.
viewModelScope.launch {
val motion =
if (mannequin.cardsToReview else MyDeckListAction.NavigateToPractice(mannequin.deckId)
_action.emit(motion)
}
I’ve additionally eliminated the “Observe” button from the house display screen, I will not be implementing it any time quickly so there isn’t any worth in it as soon as it is there.
I began my fifteenth day by doing one of the essential courses of this mission, the category that checks if a solution is right.
I must know somewhat greater than whether or not the reply is true/false. If it is right, I additionally must know if it is a precise match. To try this, I created a brand new class that represents the doable outcomes of the use case.
sealed interface CheckPracticeAnswerResponse {
knowledge class Right(val isExactAnswer: Boolean) : CheckPracticeAnswerResponse
object Incorrect : CheckPracticeAnswerResponse
enjoyable isCorrect() = that is Right
}
Then I outlined a listing of characters that I needed to disregard when checking the reply. Meaning “How?” and “How” are handled as in the event that they have been the identical factor.
personal val charsToIgnore = Regex("[?!,.;"']")
Now we come to the code that checks the reply. I begin by normalizing the typed reply and doable solutions (output), that is simply eradicating the characters to disregard.
Then I examine the normalized typed reply with the doable normalized solutions, if any of the solutions match then the person entered it accurately, in any other case it was the unsuitable reply.
override enjoyable checkAnswer(card: DeckCard, reply: String): CheckPracticeAnswerResponse {
val normalizedAnswer = reply.normalize()
val normalizedOutputs = card.outputs.normalize()
if (normalizedAnswer.isBlank())
return Incorrect
if (normalizedOutputs.any { it == normalizedAnswer })
return Right(isExactAnswer = card.outputs.any { it == reply })
return Incorrect
}
personal enjoyable String.normalize() = trim().exchange(charsToIgnore, "")
personal enjoyable Listing.normalize() = map { it.normalize() }
That is nonetheless not the ultimate model of this class however it’s adequate for now.
As a result of this class has a bit extra complicated logic than the remainder of the applying, I made a decision to jot down some unit exams for it. On this case I believe it will likely be value my time. Right here is an instance of a take a look at:
personal val card1 = card(
enter = "Potrebbe aiutarmi, per favore?",
outputs = listOf("Might you assist me, please?"),
)
@Check
enjoyable answerWithoutQuestionMarkIsCorrect() {
val reply = "Might you assist me, please"
val end result = useCase.checkAnswer(card1, reply)
assertEquals(Right(isExactAnswer = true), end result)
}
I additionally modified mine PracticeViewModel
to point out the reply within the query so it is simpler for me to check the characteristic.
What I ended up doing was including a container to the underside of the display screen. It seems whenever you sort one thing that’s not the proper reply.
- It is a crimson field for those who get it unsuitable
- It is a inexperienced field in case your reply is fairly near the precise reply
Default transition for AnimatedVisibility
broaden/collapse the container vertically and horizontally however I solely need it to broaden/collapse vertically so I’ve to vary the enter/exit transition.
@Composable
personal enjoyable InfoBox(state: PracticeState) {
AnimatedVisibility(
seen = state.infoText != null,
enter = fadeIn() + expandIn(initialSize = { IntSize(it.width, 0) }),
exit = shrinkOut(targetSize = { IntSize(it.width, 0) }) + fadeOut()
) {
val bgColor = state.infoBackgroundColorRes ?: [email protected]
Field(
modifier = Modifier
.fillMaxWidth()
.background(colorResource(id = bgColor))
.padding(horizontal = 12.dp, vertical = 20.dp)
) {
....
}
}
}
After 15 days, we lastly have our first “usable” model of the app.
If the person enters the unsuitable reply or one thing shouldn’t be the proper reply, I’ll present a field on the backside with the proper reply. When that occurs I do not need the enter to be on so someway I must disable the enter.
I additionally need to have the ability to leap to the following query utilizing the ENTER key on my keyboard, I will be utilizing this primarily on emulators so it has been a helpful characteristic for me.
I do not truly disable this area, I simply ignore new values if their enter shouldn’t be allowed to take new characters.
enjoyable onAnswerChanged(reply: String) {
if (!isAnswerFieldEnabled()) return
state.reply = reply
}
To proceed to the following query once I press the ENTER key, I’ve so as to add onKeyEvent
modifier for reply enter.
TextField(
...
modifier = Modifier
.onKeyEvent { onKeyEvent(it.nativeKeyEvent) }
)
If the primary occasion is completely different from ENTER, I simply ignore it, if not, I emit Unit
to a channel.
enjoyable onKeyEvent(keyEvent: NativeKeyEvent): Boolean {
if (keyEvent.keyCode != KeyEvent.KEYCODE_ENTER) return false
enterEventChannel.tryEmit(Unit)
return true
}
The explanation I added the channel is to forestall repeatedly urgent the ENTER key in a brief time frame. When you see beneath, I’d debug the primary enter occasion after 50 milliseconds to forestall that from occurring. I did not have this channel at first however after some testing I noticed that the ENTER key occasion is inflicting some issues and this is the reason.
personal val enterEventChannel = MutableSharedFlow(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
viewModelScope.launch {
enterEventChannel
.debounce(KEY_INPUT_DEBOUNCE_DELAY)
.accumulate { onContinue() }
}
Final on onContinue
technique I load the following query or examine the reply based mostly on the standing the display screen is on.
enjoyable onContinue() {
if (state.infoText != null) {
loadNextQuestion()
} else {
checkAnswer()
}
}
The apply display screen acquired a bit difficult and I wanted to refactor it however that is for later.
The house display screen appeared too plain so I made a decision so as to add some random background colours to my deck. The logic is straightforward so the colours can repeat, which does not make sense, nevertheless it’s okay for now.
Up till now, the checking algorithm solely checked if phrases have been equal, ignoring some characters. On at the present time, I modified the algorithm once more to contemplate the similarity.
For instance, if the reply is “tastiera” and the person enters “tastier” or “astiera”, I would like these phrases to be thought-about right.
There are definitely higher methods to do that however one straightforward manner is to make use of the Levenshtein Distance. It is an algorithm that” measure the distinction between two sequences“. It isn’t a sophisticated algorithm, this paragraph provides you with understanding about it.
I did not implement the algorithm myself, I discovered a Java model and transformed it to Kotlin. I wrapped it in WordDistanceCalculator
class.
class CheckPracticeAnswerUseCaseImpl @Inject constructor(
personal val wordDistanceCalculator: WordDistanceCalculator
)
For every output, I calculate the gap. Then I multiply the size of the phrase by the edge I outlined, now it is 0.2. Meaning the distinction may be at most 20% of the phrase’s size, in a phrase with 5 characters it may be 1 character. If the gap is lower than or equal to the edge I return the reply as right however I set isExactAnswer
to false so I do know the person typed one thing related however not the precise reply.
normalizedOutputs.forEach { output ->
val distance = wordDistanceCalculator.calculate(normalizedAnswer, output)
val threshold = (output.size * WORD_DIFF_THRESHOLD).toInt()
if (distance }
This algorithm is not good however I do not see myself altering it any extra within the close to future, here is the algorithm I used to be pondering of when describing how the apply would work.
That is additionally the day I really feel like giving up. I am unsure the right way to proceed with the mission so it would not appear to be I am motivated to proceed engaged on it. This can be a fairly widespread factor for me, it is occurred many occasions earlier than and is normally the factor that kills my tasks.
A number of months in the past, I learn “ Disagreeable necessities ” by Josh Pigford, within the article he says:
Not the whole lot you’re employed on will likely be fascinating. Lots of the core components of an actual mission would be the hardest elements that you just keep away from just like the plague.
These “nasty necessities” have been the reason for the loss of life of numerous tasks. They’re the issues we delay indefinitely.
Once I first learn this, I used to be amazed. This has occurred to me many occasions earlier than however I by no means understood why.
He continued by offering the answer:
The important thing to overcoming these nasty necessities shouldn’t be willpower, it is planning.
It is deciding “that is the place I would like this mission to finish” after which working backwards, step-by-step, to determine what must be finished. You’re pondering “listed below are the steps to get the place I need to be”.
The purpose is to not keep away from discomfort, however to cut back resolution fatigue. You preload all of the essential selections after which merely observe the plan.
What was lacking for me was “that is the place I would like this mission to finish up”. Though I outlined a imaginative and prescient in the beginning of the mission, I ended being attentive to it and have become misplaced. The subsequent steps usually are not clear.
With these updates, we have reached the top of season 4.
You probably have any feedback or strategies be at liberty to contact me on Twitter.
Keep tuned for the following updates.
photograph taken by William Warby ABOVE depart
John Wick: Chapter 4 (FREE) FULLMOVIE The Super Mario Bros Movie avatar 2 Where To Watch Creed 3 Free At Home Knock at the Cabin (2023) FullMovie Where To Watch Ant-Man 3 and the Wasp: Quantumania Cocaine Bear 2023 (FullMovie) Scream 6 Full Movie