Generic data in compose ui state - mvvm

I'm getting data from Marvel API, so the main screen you have different kinds of categories (Characters, Events, Comics etc.) When the user clicks on one of the categories, the app navigates to a list of the related data.
So I want this screen to hold different kinds of data (categories) without using a different screen for each one. Is this the best approach? and how can I do that?
code:
#kotlinx.serialization.Serializable
data class MarvelResponse(
val data:Data
)
#kotlinx.serialization.Serializable
data class Data(
var characters:List<Character>,
var series:List<Series>,
var stories:List<Story>,
var events:List<Event>,
var comics:List<Comic>,
var cartoons:List<Cartoon>
)
class DetailsViewModel #Inject constructor(
private val useCase: UseCase,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _uiState = mutableStateOf<Resource<Any>>(Resource.Loading())
val uiState = _uiState
private fun getData(category: String) {
when (category) {
"Characters" -> {
getCharacters()
}
"Comics" -> {
getComics()
}
"Series" -> {
//
}
"Stories" -> {
//
}
}
}
private fun getCharacters() {
viewModelScope.launch {
val charactersResponse = useCase.getCharactersUseCase()
_uiState.value = Resource.Success(charactersResponse)
}
}
..........
fun Details(
vm: DetailsViewModel = hiltViewModel(),
navController:NavHostController
) {
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
},
title = { Text(text = "Back") }
)
}
) { paddingValues ->
DetailsVerticalGrid(state, modifier = Modifier.padding(paddingValues))
}
}
#ExperimentalMaterialApi
#ExperimentalComposeUiApi
#Composable
fun DetailsVerticalGrid(
data: List<Any>,
modifier: Modifier = Modifier
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(30.dp),
modifier = modifier
) {
items(data.size) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
data.forEach {
DetailsGridItemCard(
image = "",
title = it.title
) {
}
}
}
}
}
}
Of course the above code will not work, I want it to work with any type of data, using a state that holds the data according to the category selected. How can I achieve that?

Related

Jetpack Compose Snackbar above Dialog

How can a Snackbar be shown above a Dialog or AlertDialog in Jetpack Compose? Everything I have tried has resulted in the snack bar being below the scrim of the dialog not to mention the dialog itself.
According to Can I display material design Snackbar in dialog? it is possible in non-Compose Android by using a custom or special (like getDialog().getWindow().getDecorView()) view, but that isn't accessible from Compose I believe (at least not without a lot of effort).
I came up with a solution that mostly works. It uses the built-in Snackbar() composable for the rendering but handles the role of SnackbarHost() with a new function SnackbarInDialogContainer().
Usage example:
var error by remember { mutableStateOf<String?>(null) }
AlertDialog(
...
text = {
...
if (error !== null) {
SnackbarInDialogContainer(error, dismiss = { error = null }) {
Snackbar(it, Modifier.padding(WindowInsets.ime.asPaddingValues()))
}
}
}
...
)
It has the following limitations:
Has to be used in place within the dialog instead of at the top level
There is no host to queue messages, instead that has to be handled elsewhere if desired
Dismissal is done with a callback (i.e. { error = null} above) instead of automatically
Actions currently do nothing at all, but that could be fixed (I had no use for them, the code do include everything necessary to render the actions I believe, but none of the interaction).
This has built-in support for avoiding the IME (software keyboard), but you may still need to follow https://stackoverflow.com/a/73889690/582298 to make it fully work.
Code for the Composable:
#Composable
fun SnackbarInDialogContainer(
text: String,
actionLabel: String? = null,
duration: SnackbarDuration =
if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite,
dismiss: () -> Unit,
content: #Composable (SnackbarData) -> Unit
) {
val snackbarData = remember {
SnackbarDataImpl(
SnackbarVisualsImpl(text, actionLabel, true, duration),
dismiss
)
}
val dur = getDuration(duration, actionLabel)
if (dur != Long.MAX_VALUE) {
LaunchedEffect(snackbarData) {
delay(dur)
snackbarData.dismiss()
}
}
val popupPosProvider by imeMonitor()
Popup(
popupPositionProvider = popupPosProvider,
properties = PopupProperties(clippingEnabled = false),
) {
content(snackbarData)
}
}
#Composable
private fun getDuration(duration: SnackbarDuration, actionLabel: String?): Long {
val accessibilityManager = LocalAccessibilityManager.current
return remember(duration, actionLabel, accessibilityManager) {
val orig = when (duration) {
SnackbarDuration.Short -> 4000L
SnackbarDuration.Long -> 10000L
SnackbarDuration.Indefinite -> Long.MAX_VALUE
}
accessibilityManager?.calculateRecommendedTimeoutMillis(
orig, containsIcons = true, containsText = true, containsControls = actionLabel != null
) ?: orig
}
}
/**
* Monitors the size of the IME (software keyboard) and provides an updating
* PopupPositionProvider.
*/
#Composable
private fun imeMonitor(): State<PopupPositionProvider> {
val provider = remember { mutableStateOf(ImePopupPositionProvider(0)) }
val context = LocalContext.current
val decorView = remember(context) { context.getActivity()?.window?.decorView }
if (decorView != null) {
val ime = remember { WindowInsetsCompat.Type.ime() }
val bottom = remember { MutableStateFlow(0) }
LaunchedEffect(Unit) {
while (true) {
bottom.value = ViewCompat.getRootWindowInsets(decorView)?.getInsets(ime)?.bottom ?: 0
delay(33)
}
}
LaunchedEffect(Unit) {
bottom.collect { provider.value = ImePopupPositionProvider(it) }
}
}
return provider
}
/**
* Places the popup at the bottom of the screen but above the keyboard.
* This assumes that the anchor for the popup is in the middle of the screen.
*/
private data class ImePopupPositionProvider(val imeSize: Int): PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect, windowSize: IntSize,
layoutDirection: LayoutDirection, popupContentSize: IntSize
) = IntOffset(
anchorBounds.left + (anchorBounds.width - popupContentSize.width) / 2, // centered on screen
anchorBounds.top + (anchorBounds.height - popupContentSize.height) / 2 + // centered on screen
(windowSize.height - imeSize) / 2 // move to the bottom of the screen
)
}
private fun Context.getActivity(): Activity? {
var currentContext = this
while (currentContext is ContextWrapper) {
if (currentContext is Activity) {
return currentContext
}
currentContext = currentContext.baseContext
}
return null
}
private data class SnackbarDataImpl(
override val visuals: SnackbarVisuals,
val onDismiss: () -> Unit,
) : SnackbarData {
override fun performAction() { /* TODO() */ }
override fun dismiss() { onDismiss() }
}
private data class SnackbarVisualsImpl(
override val message: String,
override val actionLabel: String?,
override val withDismissAction: Boolean,
override val duration: SnackbarDuration
) : SnackbarVisuals

Flow invariant is violated

Resources:::
ViewModel -->
#ExperimentalCoroutinesApi
#HiltViewModel
class TestimonialViewModel
#Inject constructor(
private val testimonialRepo: TestimonialRepo,
private val authRepo: AuthRepo,
val networkHelper: NetworkHelper,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
var title by mutableStateOf(UiString.Resource(R.string.testimonial_title_create))
private set
var data by mutableStateOf(TestimonialData())
private set
private var curInx = 0
private val _areInputsValid = MutableStateFlow(false)
val areInputsValid = _areInputsValid.asStateFlow()
private val _mutableState = MutableStateFlow<TestimonialGetState>(TestimonialGetState.Loading)
val state = _mutableState.asStateFlow()
private val _stateChannel = Channel<TestimonialSubmitState>()
val stateChannel = _stateChannel.receiveAsFlow()
init {
onLoad()
}
fun onLoad() {
if (networkHelper.isConnected()) {
authRepo.getUser()?.let {
loadTestimonial(userId = it.uid)
}
} else {
_mutableState.value = TestimonialGetState.NoInternet
}
}
private fun loadTestimonial(userId: String) {
viewModelScope.launch {
testimonialRepo.getTestimonial(userId).catch { ex ->
//_mutableState.value = TestimonialGetState.Error(ex)
_mutableState.value = TestimonialGetState.Empty
}.collect {
if (it.id.isNotBlank())
title = UiString.Resource(R.string.testimonial_title_edit)
data = TestimonialData(
id = it.id,
type = it.type,
title = it.title,
testimonial = it.testimonial,
role = it.user.role,
org = it.user.org
)
when (it.type) {
TestimonialType.TEXT -> {
}
TestimonialType.IMAGE -> {
data.imgMedia = it.media.toMutableStateList()
}
TestimonialType.VIDEO -> {
data.vdoMedia = it.media.toMutableStateList()
}
}
_mutableState.value = TestimonialGetState.Success(it)
}
}
}
private fun updateTestimonial(netTestimonial: Testimonial) {
viewModelScope.launch {
testimonialRepo.updateTestimonial(netTestimonial)
.catch { ex ->
_stateChannel.send(TestimonialSubmitState.Error(ex))
}.collect {
clearErrors()
_stateChannel.send(TestimonialSubmitState.Success(it))
}
}
}
fun onEvent(event: TestimonialEvent) {
when (event) {
is TestimonialEvent.OnTypeChange -> {
data = data.copy(type = event.type)
_areInputsValid.value = validateTestimonial(event.type)
}
is TestimonialEvent.OnTitleChange -> {
data = data.copy(title = event.title)
data.titleError = onValidateText(event.title, 4, 64)
_areInputsValid.value = validateTestimonial(data.type)
}
is TestimonialEvent.OnTestimonialChange -> {
data = data.copy(testimonial = event.testimonial)
data.testimonialError = onValidateText(event.testimonial, 32, 256)
_areInputsValid.value = validateTestimonial(data.type)
}
is TestimonialEvent.OnOrgChange -> {
data = data.copy(org = event.org)
data.orgError = onValidateText(event.org, 4, 32)
_areInputsValid.value = validateTestimonial(data.type)
}
is TestimonialEvent.OnRoleChange -> {
data = data.copy(role = event.role)
data.roleError = onValidateText(event.role, 2, 32)
_areInputsValid.value = validateTestimonial(data.type)
}
is TestimonialEvent.OnMediaIndex -> {
curInx = event.inx
}
is TestimonialEvent.OnImageSelect -> {
if (event.url != null) {
data.imgMedia[curInx] = data.imgMedia[curInx].copy(localUrl = event.url)
}
data.imgError = onValidateMedia(data.imgMedia)
_areInputsValid.value = validateTestimonial(data.type)
}
is TestimonialEvent.OnImageClear -> {
data.imgMedia[event.inx] = data.imgMedia[event.inx].copy(localUrl = null, url = "")
data.imgError = UiString.Resource(R.string.the_media_can_not_be_blank)
_areInputsValid.value = validateTestimonial(data.type)
}
is TestimonialEvent.OnVideoSelect -> {
if (event.url != null) {
data.vdoMedia[curInx] = data.vdoMedia[curInx].copy(localUrl = event.url)
}
data.vdoError = onValidateMedia(data.vdoMedia)
_areInputsValid.value = validateTestimonial(data.type)
}
is TestimonialEvent.OnVideoClear -> {
data.vdoMedia[event.inx] = data.vdoMedia[event.inx].copy(localUrl = null, url = "")
data.vdoError = UiString.Resource(R.string.the_media_can_not_be_blank)
_areInputsValid.value = validateTestimonial(data.type)
}
is TestimonialEvent.OnSubmit -> {
submit()
}
}
}
private fun submit() {
authRepo.getUser()?.let {
updateTestimonial(
Testimonial(
id = data.id,
type = data.type,
title = data.title.trim(),
testimonial = data.testimonial.trim(),
userId = it.uid,
user = TestimonialUser(
name = it.name!!,
role = data.role.trim(),
org = data.org.trim(),
url = it.photoUrl.toString()
),
media = when (data.type) {
TestimonialType.TEXT -> listOf()
TestimonialType.IMAGE -> data.imgMedia
TestimonialType.VIDEO -> data.vdoMedia
}
)
)
}
}
private fun clearErrors() {
data.titleError = UiString.Empty
data.testimonialError = UiString.Empty
data.roleError = UiString.Empty
data.orgError = UiString.Empty
data.imgError = UiString.Empty
data.vdoError = UiString.Empty
}
private fun onValidateText(value: String, min: Int, max: Int): UiString {
return when {
value.isBlank() -> UiString.Resource(R.string.the_field_can_not_be_blank)
value.length > max -> UiString.Resource(R.string.data_length_more, max)
value.length in 1 until min -> UiString.Resource(R.string.data_length_less, min)
else -> UiString.Empty
}
}
private fun onValidateMedia(mediaList: List<Media>): UiString {
return if (validateMedia(mediaList)) UiString.Empty
else UiString.Resource(R.string.the_media_can_not_be_blank)
}
private fun validateMedia(mediaList: List<Media>): Boolean {
return mediaList.find { it.localUrl == null && it.url.isBlank() } == null
}
private fun validateTestimonial(type: TestimonialType): Boolean {
return when (type) {
TestimonialType.TEXT -> validateData(
data.testimonial.isNotBlank() && data.testimonialError is UiString.Empty
)
TestimonialType.IMAGE -> validateData(
validateMedia(data.imgMedia) && data.imgError is UiString.Empty
)
TestimonialType.VIDEO -> validateData(
validateMedia(data.vdoMedia) && data.vdoError is UiString.Empty
)
}
}
private fun validateData(typeValidation: Boolean): Boolean {
return (typeValidation && data.title.isNotBlank() && data.titleError is UiString.Empty
&& data.org.isNotBlank() && data.orgError is UiString.Empty
&& data.role.isNotBlank() && data.roleError is UiString.Empty
)
}
}
TestimonialsSubmitState ->
sealed class TestimonialSubmitState {
object Loading : TestimonialSubmitState()
class Error(val error: Throwable) : TestimonialSubmitState()
class Success(val status: Status) : TestimonialSubmitState()
}
UI Component ->
#OptIn(ExperimentalCoroutinesApi::class)
#Composable
fun TestimonialForm(
editViewModel: TestimonialViewModel = hiltViewModel(),
paddingValues: PaddingValues,
onNavigateTstHome: () -> Unit,
) {
val focusRequester = remember { FocusRequester() }
val radioButtonList = listOf(
TestimonialType.TEXT, TestimonialType.IMAGE, TestimonialType.VIDEO
)
var showCustomDialogWithResult by remember { mutableStateOf(false) }
val selectedOption = radioButtonList.find {
it == editViewModel.data.type
} ?: radioButtonList[0]
val focusManager = LocalFocusManager.current
val scrollState = rememberScrollState()
val areInputsValid by editViewModel.areInputsValid.collectAsStateWithLifecycle()
val events = editViewModel.stateChannel.collectAsState(initial = TestimonialSubmitState.Loading)
val event = events.value
Column(
modifier = Modifier
.fillMaxWidth()
.padding(
top = paddingValues.calculateTopPadding(),
start = spacing.medium,
end = spacing.medium
)
.verticalScroll(scrollState), verticalArrangement = Arrangement.Center
) {
Spacer(modifier = Modifier.height(spacing.medium))
TestimonialsRadioButton(selectionTypeText = "Testimonial Type",
radioButtonList = radioButtonList,
selectedOption = selectedOption,
content = {
ValidatedTextField(
value = editViewModel.data.testimonial,
onValueChange = {
editViewModel.onEvent(
TestimonialEvent.OnTestimonialChange(
it
)
)
},
label = "Testimonial",
showError = editViewModel.data.testimonialError !is UiString.Empty,
errorMessage = editViewModel.data.testimonialError,
keyboardOptions = if (editViewModel.data.testimonial.length < 512) {
KeyboardOptions(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Default
)
} else {
KeyboardOptions(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Next
)
},
keyboardActions = KeyboardActions(onNext = {
focusManager.clearFocus()
}),
modifier = Modifier.height(boxSize.extraMedium)
)
},
titleTestimonial = {
ValidatedTextField(
value = editViewModel.data.title,
onValueChange = { editViewModel.onEvent(TestimonialEvent.OnTitleChange(it)) },
label = "Title",
showError = editViewModel.data.titleError !is UiString.Empty,
errorMessage = editViewModel.data.titleError,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(onNext = {
focusManager.moveFocus(FocusDirection.Down)
})
)
},
onOptionSelected = {
editViewModel.onEvent(TestimonialEvent.OnTypeChange(it))
})
Spacer(modifier = Modifier.height(spacing.medium))
ValidatedTextField(
value = editViewModel.data.org,
onValueChange = { editViewModel.onEvent(TestimonialEvent.OnOrgChange(it)) },
label = "Organisation",
showError = editViewModel.data.orgError !is UiString.Empty,
errorMessage = editViewModel.data.orgError,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(onNext = {
focusManager.moveFocus(FocusDirection.Down)
})
)
Spacer(modifier = Modifier.height(spacing.medium))
ValidatedTextField(
value = editViewModel.data.role,
onValueChange = { editViewModel.onEvent(TestimonialEvent.OnRoleChange(it)) },
label = "Role",
showError = editViewModel.data.roleError !is UiString.Empty,
errorMessage = editViewModel.data.roleError,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
modifier = Modifier.focusRequester(focusRequester = focusRequester),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
if (selectedOption == radioButtonList[1]) {
Spacer(modifier = Modifier.height(spacing.medium))
TestGetImage()
} else if (selectedOption == radioButtonList[2]) {
Spacer(modifier = Modifier.height(spacing.medium))
TestGetVideo()
}
androidx.compose.material.Button(
onClick = {
editViewModel.onEvent(TestimonialEvent.OnSubmit)
showCustomDialogWithResult = !showCustomDialogWithResult
},
modifier = Modifier
.fillMaxWidth(1f)
.padding(spacing.medium),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Blue, contentColor = Color.White
),
enabled = areInputsValid
) {
Text(text = "Submit", style = MaterialTheme.typography.labelLarge)
}
if (showCustomDialogWithResult) {
when (event) {
is TestimonialSubmitState.Error -> {
Log.d("validate", "Error -> ${event.error.message} and ${event.error.cause}")
}
TestimonialSubmitState.Loading -> {
LoadingAnimation()
}
is TestimonialSubmitState.Success -> {
OnSuccessfulSubmit(onDismiss = {
showCustomDialogWithResult = !showCustomDialogWithResult
}, onNavigateTstHome = onNavigateTstHome, onPositiveClick = {
editViewModel.onEvent(TestimonialEvent.OnSubmit)
onNavigateTstHome()
})
}
}
}
}
}
SuccessfulSubmit Dialog ->
Here, LaunchedEffect block is just for testing purpose.
#Composable
fun OnSuccessfulSubmit(
onDismiss: () -> Unit,
onNavigateTstHome: () -> Unit,
onPositiveClick: () -> Unit,
) {
var underReview by remember {
mutableStateOf(true)
}
LaunchedEffect(key1 = Unit, block = {
delay(2000)
underReview = false
delay(3000)
onNavigateTstHome()
})
Dialog(
onDismissRequest = onDismiss, properties = DialogProperties(dismissOnClickOutside = false)
) {
SuccessfulCard(onClick = onPositiveClick, underReview = underReview)
}
}
Error --->
When I click on Submit Button --->
Log --->
Question -->
What is this error in Log... Code part related to it --->
_stateChannel, stateChannel, updateTestimonial(), submit() ---> inherited in Testimonial form ----> last 2nd Button Component and below it event calling is DONE.
Sorry for bad structuring of the question.
Thank you.
Things I tried ---> events variable in TestimonialForm, I tried to make collectAsState( initial = null )
Also tried LaunchedEffect for when case as suggested in other similar question on Stackoverflow, but got Composable invocation error.

How can i make Http request again in retrofit to get updated data from api

I want to update the data I'm getting from api to display it in my lazy column, I'm trying to add swipe down to refresh functionality.
I'm getting the data from my viewmodel
#HiltViewModel
class MatchesViewModel #Inject constructor(
private val matchRepository: MatchRepository
): ViewModel() {
val response: MutableState<ApiState> = mutableStateOf(ApiState.Empty)
init {
getAllMatches()
}
private fun getAllMatches() = viewModelScope.launch {
cricketRepository.getAllMatches().onStart {
response.value = ApiState.Loading
} .catch {
response.value = ApiState.Failure(it)
}.collect {
response.value = ApiState.Success(it) }
}
}
then i made new kotlin file where I'm checking if I'm getting the data and passing it in my lazy column
#Composable
fun MainScreen(viewModel: MatchesViewModel = hiltViewModel()){
when (val result = viewModel.response.value){
is ApiState.Success -> {
HomeScreen(matches = result.data.data)
}
is ApiState.Loading -> {
}
is ApiState.Empty -> {
}
is ApiState.Failure -> {
}
}
}
i want to know how can i make the request again to get the updated data
after some googling i found out you can retry api calls with okhttp interceptors but could'nt find any documentation or tutorial to retry calls with interceptor
You can try Swipe to Refresh with the accompanist dependencie
implementation "com.google.accompanist:accompanist-swiperefresh:0.26.5-rc"
Implementation would be like this
val swipeRefreshState = rememberSwipeRefreshState(false)
SwipeRefresh(
state = swipeRefreshState,
onRefresh = {
swipeRefreshState.isRefreshing = false
viewModel.<FUNCTION TO LOAD YOUR DATA>
},
indicator = { swipeRefreshState, trigger ->
SwipeRefreshIndicator(
// Pass the SwipeRefreshState + trigger through
state = swipeRefreshState,
refreshTriggerDistance = trigger,
// Enable the scale animation
scale = true,
// Change the color and shape
backgroundColor = <UPDATE CIRLE COLOR>,
contentColor = <RELOADING ICON COLOR>,
shape = RoundedCornerShape(50),
)
}
) {
<CONTENT OF YOUR SCREEN>
}

swashbuckle openapi 3 write example and description for the dynamically generated model classes

My model properties definition is coming from a json file so using reflection to write the classes to be shown under schema on resulting swagger page.
foreach (var model in Models)
{
if (!ModelTypes.ContainsKey(model.Key))
{
anyNonCompiledModel = true;
BuildModelCodeClass(modelComponentBuilder, model.Value);//Build model classes
}
}
BuildModelCodeEnd(modelComponentBuilder);
if (anyNonCompiledModel)
{
CSharpCompiler compiler = new CSharpCompiler();
compiler.AddReference(typeof(object));
compiler.AddReference(typeof(ResourceFactory));
compiler.AddReference(typeof(System.Runtime.Serialization.DataContractResolver));
compiler.AddReference(typeof(System.Runtime.Serialization.DataContractAttribute));
var types = compiler.Compiler(modelComponentBuilder.ToString()); //write model classes
foreach (var type in types)
{
ModelTypes.Add(type.Name, type);
}
}
public void BuildModelCodeClass(StringBuilder modelComponentBuilder, MetadataModelEntity model)
{
modelComponentBuilder.AppendLine($"public class {model.Name} {{");
foreach (var p in model.Data.Properties)
{
if (p.Obsoleted) continue;
if (p.Type.Type == "array")
{
modelComponentBuilder.AppendLine($" public {p.Type.ArrayType.ObjectName}[] {p.Name} {{get;set;}}");
}
else
{
//primitive types
modelComponentBuilder.AppendLine($" public {p.Type.ObjectName} {p.Name} {{get;set;}}");
}
}
modelComponentBuilder.AppendLine(
#"}
");
}
If i provide the description and example like following (in BuildModelCodeClass, inside the loop) then the example and description displays for me.
if (!string.IsNullOrWhiteSpace((string)p.Example))
{
modelComponentBuilder.AppendLine($" ///<example>{p.Example}</example>");
}
if (!string.IsNullOrWhiteSpace((string)p.Description))
{
modelComponentBuilder.AppendLine($" ///<description>{p.Description}</description>");
}
However, i dont want to do above.
I want to write my models via the open api and not via the C# Compiler, is it possible?
I want to show example and description via schema (may be under paths some where). How can i do this? Context has my models info available that i can interact with here.
public class SwaggerDocumentFilter : IDocumentFilter
{
SwaggerDocument _swaggerDocument;
public SwaggerDocumentFilter(object apiConfigure)
{
_swaggerDocument = ((ApiGatewayConfiguration)apiConfigure).SwaggerDocument;
}
public void Apply(OpenApiDocument document, DocumentFilterContext context)
{
if (document.Info.Extensions == null || !document.Info.Extensions.ContainsKey(SwaggerEndpoint.ExtensionDocName)) return;
var openIdString = document.Info.Extensions[SwaggerEndpoint.ExtensionDocName] as OpenApiString;
if (openIdString == null) return;
var docName = openIdString.Value;
SwaggerEndpoint endpoint = _swaggerDocument.SwaggerEndpoints.SingleOrDefault(x => x.Name == docName);
if (endpoint == null) return;
//Add server objects
document.Servers = endpoint.ServerObjects;
//Add Tags objects
document.Tags = endpoint.Tags;
//Set swagger paths objects
var pathsObjects = _swaggerDocument.GetPathsObject(docName, context);
if (pathsObjects.IsValid())
{
pathsObjects.ToList().ForEach(
item => document.Paths.Add(item.Key, item.Value)
);
}
//Add Schema components
//Add Example/Examples
}
}
Following helped
https://github.com/domaindrivendev/Swashbuckle.WebApi/issues/162
AddSchemaExamples.cs
public class AddSchemaExamples : ISchemaFilter
{
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
if (type == typeof(Product))
{
schema.example = new Product
{
Id = 123,
Type = ProductType.Book,
Description = "Treasure Island",
UnitPrice = 10.0M
};
}
}
}
SwaggerConfig.cs
httpConfig
.EnableSwagger(c =>
{
c.SchemaFilter<AddSchemaExamples>()
});
My implementation for the Apply since model is dynamic
if (model != null)
{
schema.Description = model.Description;
foreach (var p in schema.Properties)
{
var mp = model.Data.Properties.SingleOrDefault(x => x.Name == p.Key);
if (mp != null)
{
if (!string.IsNullOrWhiteSpace(mp.Description))
{
p.Value.Description = mp.Description;
}
if(!string.IsNullOrWhiteSpace(mp.Example))
{
p.Value.Example =
new Microsoft.OpenApi.Any.OpenApiString(mp.Example.ToString());
}
}
}
}

LIveData is observed more than twice everytime from acivity and emitting previous data

I follow MVVM Login API, with Retrofit ,My problem is livedata is observed more than twice and always emitting previous response when observed from activity, But inside Repository its giving correct response
I tried a lot of solutions from stackoverflow and other websites but still no luck, Tried removing observers also but still getting previous data ,so plz suggest a working solution, I will post my code below,
LoginActivity.kt
private lateinit var loginViewModel: LoginViewModel
loginViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(
LoginViewModel::class.java
)
loginViewModel.login(userEmail, pwd)
loginViewModel.getLoginRepository().observe(this, Observer {
val loginResult = it ?: return#Observer
val accessToken = loginResult.user?.jwtToken.toString()
})
val statusMsgObserver = Observer<String> { statusMsg ->
showToast(statusMsg)
})
val errorMsgObserver = Observer<String> { errorMsg ->
// Update the UI
showToast(errorMsg)
})
loginViewModel.getStatusMessage()?.observe(this, statusMsgObserver)
loginViewModel.getErrorStatusMessage()?.observe(this, errorMsgObserver)
LoginViewModel.kt:
class LoginViewModel: ViewModel() {
private var loginRepository: LoginRepository? = null
private var _mutableLiveData = MutableLiveData<LoginAPIResponse?>()
val liveData: LiveData<LoginAPIResponse?> get() = _mutableLiveData
private var responseMsgLiveData:MutableLiveData<String>?= null
private var errorResponseMsgLiveData:MutableLiveData<String>?= null
fun login(username: String, password: String) {
loginRepository = LoginRepository.getInstance()!!
/* Query data from Repository */
//val _mutableLiveData: MutableLiveData<Response<LoginAPIResponse?>?>? = loginRepository?.doLogin(username, password)
_mutableLiveData = loginRepository?.doLogin(username, password)!!
responseMsgLiveData = loginRepository?.respMessage!!
errorResponseMsgLiveData = loginRepository?.loginResponseErrorData!!
}
fun getLoginRepository(): LiveData<LoginAPIResponse?> {
return liveData
}
fun getStatusMessage(): LiveData<String>? {
return responseMsgLiveData
}
fun getErrorStatusMessage(): LiveData<String>? {
return errorResponseMsgLiveData
}
}
LoginRepository.kt:
class LoginRepository {
private val loginApi: ApiEndpoints = RetrofitService.createService(ApiEndpoints::class.java)
val responseData = MutableLiveData<LoginAPIResponse?>()
var respMessage = MutableLiveData<String>()
var loginResponseErrorData = MutableLiveData<String>()
fun doLogin(username: String, password: String)
: MutableLiveData<LoginAPIResponse?> {
respMessage.value = null
loginResponseErrorData.value = null
val params = JsonObject()
params.addProperty("email", username)
params.addProperty("password",password)
val jsonParams = JsonObject()
jsonParams.add("user",params)
loginApi.loginToServer(jsonParams).enqueue(object : Callback<LoginAPIResponse?> {
override fun onResponse( call: Call<LoginAPIResponse?>, response: Response<LoginAPIResponse?> ) {
responseData.value = response.body()
respMessage.value = RetrofitService.handleError(response.code())
val error = response.errorBody()
if (!response.isSuccessful) {
val errorMsg = error?.charStream()?.readText()
println("Error Message: $errorMsg")
loginResponseErrorData.value = errorMsg
} else {
println("API Success -> Login, $username, ${response.body()?.user?.email.toString()}")
}
}
override fun onFailure(call: Call<LoginAPIResponse?>, t: Throwable) {
println("onFailure:(message) "+t.message)
loginResponseErrorData.value = t.message
responseData.value = null
}
})
return responseData
}
companion object {
private var loginRepository: LoginRepository? = null
internal fun getInstance(): LoginRepository? {
if (loginRepository == null) {
loginRepository = LoginRepository()
}
return loginRepository
}
}
}
In onDestroy(),I have removed the observers,
override fun onDestroy() {
super.onDestroy()
loginViewModel.getLoginRepository()?.removeObservers(this)
this.viewModelStore.clear()
}
In LoginActivity, when I observe loginResult it gives previous emitted accessToken first and then again called and giving current accessToken, Similarly observer is called more than twice everytime.
But inside repository,its giving recent data, plz check my code and suggest where I have to correct to get correct recent livedata
Finally i found the solution, In LoginRepository, I declared responseData outside doLogin(), It should be declared inside doLogin()
Since it was outside the method, it always gave previous data first and then current data,
Once I declare inside method problem was solved and now it is working Perfect!!!