I have a problem with the code in ViewModel + Kotlin Coroutines + LiveData - kotlin-coroutines

I have a ViewModel
#HiltViewModel
class LoginViewModel #Inject constructor(
private val apiRepository: ApiRepository
) : ViewModel() {
private val account = MutableLiveData<String>("123")
private val password = MutableLiveData<String>("123")
val message: MutableLiveData<String> = MutableLiveData()
var loginResult: LiveData<Resource<UserInfo>> = MutableLiveData()
fun signIn() {
if (TextUtils.isEmpty(account.value)) {
message.postValue("Please enter your account")
return
}
if (TextUtils.isEmpty(password.value)) {
message.postValue("please enter your password")
return
}
// In this code, it doesn’t work. I think it’s because I didn’t observe it.
// Is there any better way to write it here?
loginResult = apiRepository.signIn(account.value!!, password.value!!)
}
fun inputAccount(accountValue: String) {
account.value = accountValue
}
fun inputPassword(passwordValue: String) {
password.value = passwordValue
}
}
This is my interface
#AndroidEntryPoint
class LoginActivity : BaseActivity<ActivityLoginBinding>() {
private val viewModel: LoginViewModel by viewModels()
......
override fun initEvent() {
binding.account.editText!!.addTextChangedListener { viewModel.inputAccount(it.toString()) }
binding.password.editText!!.addTextChangedListener { viewModel.inputPassword(it.toString()) }
binding.signIn.setOnClickListener {
viewModel.signIn()
}
}
override fun setupObservers() {
viewModel.message.observe(this) {
Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
}
/**
* There will be no callback here, I know it’s because I’m observing
* `var loginResult: LiveData<Resource<UserInfo>> = MutableLiveData()`
* instead of `apiRepository.signIn(account.value!!, password.value!!)`
* because it was reassigned
*/
viewModel.loginResult.observe(this) {
Log.d("TAG", "setupObservers: $it")
}
}
}
So I adjusted the code a bit
LoginViewModel.signIn
fun signIn(): LiveData<Resource<UserInfo>>? {
if (TextUtils.isEmpty(account.value)) {
message.postValue("Please enter your account")
return null
}
if (TextUtils.isEmpty(password.value)) {
message.postValue("please enter your password")
return null
}
return apiRepository.signIn(account.value!!, password.value!!)
}
LoginActivity.initEvent
override fun initEvent() {
binding.signIn.setOnClickListener {
viewModel.signIn()?.observe(this) {
Log.d("TAG", "setupObservers: $it")
}
}
}
I have checked the official documents of LiveData, and all call livedata{} during initialization. There has been no re-assignment, but if you log in, you cannot directly start the application and request the network.
coroutines doucument
Although I finally achieved my results, I think this is not the best practice, so I want to ask for help!
Supplementary code
ApiRepository
class ApiRepository #Inject constructor(
private val apiService: ApiService
) : BaseRemoteDataSource() {
fun signIn(account: String, password: String) =
getResult { apiService.signIn(account, password) }
}
BaseRemoteDataSource
abstract class BaseRemoteDataSource {
protected fun <T> getResult(call: suspend () -> Response<T>): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
try {
val response = call.invoke()
if (response.isSuccessful) {
val body = response.body()
if (body != null) emit(Resource.success(body))
} else {
emit(Resource.error<T>(" ${response.code()} ${response.message()}"))
}
} catch (e: Exception) {
emit(Resource.error<T>(e.message ?: e.toString()))
}
}
}

Or i write like this
fun signIn() {
if (TextUtils.isEmpty(account.value)) {
message.postValue("Please enter your account")
return
}
if (TextUtils.isEmpty(password.value)) {
message.postValue("please enter your password")
return
}
viewModelScope.launch {
repository.signIn(account.value, password.value).onEach {
loginResult.value = it
}
}
}
But I think this is not perfect

Related

Spring RouterFunction endpoint with coroutines coRouter return 404

I have a code
#Configuration
class RouteConfiguration {
#Bean
fun apiRouter(handler: UserHandler): RouterFunction<ServerResponse> {
return router {
"/api".nest {
GET("/users") { ok().bodyValue(handler.getAllUsers()) }
GET("/users/{id}") {
val user: User? = handler.getUserById(it.pathVariable("id").toInt())
if (user != null) {
ok().bodyValue(user)
} else {
ok().build()
}
}
POST("/users") {
ok().bodyValue(handler.createUser(it.bodyToMono(User::class.java).block()))
}
}
}
}
}
and this code works fine.
So i want use coroutines inside handling functions:
#Configuration
class RouteConfiguration {
#Bean
fun apiRouter(handler: UserHandler): RouterFunction<ServerResponse> {
return coRouter {
"/api".nest {
GET("/users") { ok().bodyAndAwait(handler.getAllUsers()) }
GET("/users/{id}") {
val user: User? = handler.getUserById(it.pathVariable("id").toInt())
if (user != null) {
ok().bodyValueAndAwait(user)
} else {
ok().buildAndAwait()
}
}
POST("/users") {
ok().bodyValueAndAwait(handler.createUser(it.bodyToMono(User::class.java).awaitSingle()))
}
}
}
}
}
So after i change router RouterFunctionDsl to coRouter CoRouterFunctionDsl my endpoints start return 404 NOT FOUND and IDEAD also can't find these endpoints.
What should i do to set up coRouter in a correct way.

Dynamic DataGridViewComboBox Column

I am trying to make a DataGridViewComboBox Column that will accept new data and add it to it's list items.
I am building this for a quoting system I sort of did this another way but I just couldn't get it to function reliably.
I keep getting a System.NullReferenceException on the OnSelectedIndexChanged.
I want to be able to type a value into the the ComboBox cell have it added to the list items in order.
Can someone help me out on this?
#region DataGridViewVariableComboBoxColumn
public class DataGridViewVariableComboBoxColumn : DataGridViewComboBoxColumn
{
[Browsable(true)]
[Category("Behavior")]
[DefaultValue(false)]
public bool AddNewItems { get; set; }
private System.Windows.Forms.AutoCompleteMode comboxAutoCompleteMode;
[Browsable(true)]
[Category("Behavior")]
[DefaultValue(false)]
public System.Windows.Forms.AutoCompleteMode ComboBoxAutoCompleteMode
{
get { return comboxAutoCompleteMode; }
set
{
comboxAutoCompleteMode = value;
}
}
public DataGridViewVariableComboBoxColumn()
{
this.CellTemplate = new DataGridViewVariableComboBoxCell();
}
public override DataGridViewCell CellTemplate
{
get
{
return base.CellTemplate;
}
set
{
if (value != null &&
!value.GetType().IsAssignableFrom(typeof(DataGridViewVariableComboBoxCell)))
{
throw new InvalidCastException("Must be a DataGridViewVariableComboBoxCell");
}
base.CellTemplate = value;
}
}
public override object Clone()
{
var c = (DataGridViewVariableComboBoxColumn)base.Clone();
if (c != null)
{
c.AddNewItems = this.AddNewItems;
c.ComboBoxAutoCompleteMode = this.ComboBoxAutoCompleteMode;
c.DisplayStyle = this.DisplayStyle;
c.FlatStyle = this.FlatStyle;
}
return c;
}
}
public class DataGridViewVariableComboBoxCell : DataGridViewComboBoxCell
{
public DataGridViewVariableComboBoxCell() : base()
{
}
public override object Clone()
{
DataGridViewVariableComboBoxCell cell = base.Clone() as DataGridViewVariableComboBoxCell;
return cell;
}
public override void InitializeEditingControl(int rowIndex, object initialFormattedValue, DataGridViewCellStyle dataGridViewCellStyle)
{
try
{
base.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle);
DataGridViewVariableComboBoxEditingControl TheControl = (DataGridViewVariableComboBoxEditingControl)DataGridView.EditingControl;
var c = (DataGridViewVariableComboBoxColumn)this.OwningColumn;
TheControl.OwningColumn = this.OwningColumn;
TheControl.AddNewItems = c.AddNewItems;
TheControl.EditingComboBoxAutoCompleteMode = c.ComboBoxAutoCompleteMode;
TheControl.EditingAutoCompleteSource = AutoCompleteSource.ListItems;
TheControl.EditingComboBoxStyle = ComboBoxStyle.DropDown;
if (TheControl != null)
{
TheControl.SelectedIndex = 0;
}
}
catch { }
}
public override Type EditType
{
get
{
return typeof(DataGridViewVariableComboBoxEditingControl);
}
}
public override Type ValueType
{
get
{
// Return the type of the value that DataGridViewVariableComboBoxCell contains.
return typeof(string);
}
}
}
internal class DataGridViewVariableComboBoxEditingControl : DataGridViewComboBoxEditingControl, IDataGridViewEditingControl
{
private DataGridView dataGridViewControl;
private int rowIndex;
private bool valueChanged = false;
public System.Windows.Forms.AutoCompleteMode EditingComboBoxAutoCompleteMode
{
get
{return this.AutoCompleteMode; }
set
{
this.AutoCompleteMode = value;
}
}
public ComboBoxStyle EditingComboBoxStyle
{
get
{ return this.DropDownStyle; }
set
{
this.DropDownStyle = value;
}
}
public AutoCompleteSource EditingAutoCompleteSource
{
get
{ return this.AutoCompleteSource; }
set
{
this.AutoCompleteSource = value;
}
}
public DataGridViewVariableComboBoxEditingControl() : base()
{
}
public bool AddNewItems { get; set; }
public DataGridViewColumn OwningColumn { get; set; }
private string editingControlFormattedValue;
public string EditingControlFormattedValue
{
get
{
return editingControlFormattedValue;
}
set
{
editingControlFormattedValue = value;
}
}
public object GetEditingControlFormattedValue(DataGridViewDataErrorContexts context)
{
editingControlFormattedValue = this.Text;
if (this.SelectedIndex == -1 && this.AddNewItems && editingControlFormattedValue != "")
{
List<string> list = new List<string>();
if (this.Items.Count > 0)
{
list = this.Items.Cast<string>()
.Select(x => x)
.ToList();
}
else
{
list = new List<string>();
}
if (!list.Contains(editingControlFormattedValue) && editingControlFormattedValue != "")
{
list.Add(editingControlFormattedValue);
list = list.Distinct().OrderBy(o => o).ToList();
}
((DataGridViewVariableComboBoxColumn)OwningColumn).Items.Clear();
((DataGridViewVariableComboBoxColumn)OwningColumn).Items.AddRange(list.ToArray());
this.Items.Clear();
this.Items.AddRange(list.ToArray());
int index = ((DataGridViewVariableComboBoxColumn)OwningColumn).Items.IndexOf(editingControlFormattedValue);
EditingControlValueChanged = true;
//base.SelectedIndex = index;
//this.Text = text;
// EditingControlDataGridView.NotifyCurrentCellDirty(true);
}
return editingControlFormattedValue;
}
public DataGridView EditingControlDataGridView
{
get
{
return dataGridViewControl;
}
set
{
dataGridViewControl = value;
}
}
public int EditingControlRowIndex
{
get
{
return rowIndex;
}
set
{
rowIndex = value;
}
}
public bool EditingControlWantsInputKey(
Keys key, bool dataGridViewWantsInputKey)
{
// Let the DateTimePicker handle the keys listed.
switch (key & Keys.KeyCode)
{
case Keys.Left:
case Keys.Up:
case Keys.Down:
case Keys.Right:
case Keys.Home:
case Keys.End:
case Keys.PageDown:
case Keys.PageUp:
return true;
default:
return false;
}
}
// Implements the IDataGridViewEditingControl.PrepareEditingControlForEdit
// method.
public void PrepareEditingControlForEdit(bool selectAll)
{
// No preparation needs to be done.
}
// Implements the IDataGridViewEditingControl
// .RepositionEditingControlOnValueChange property.
public bool RepositionEditingControlOnValueChange
{
get
{
return false;
}
}
// Implements the IDataGridViewEditingControl
// .EditingControlValueChanged property.
public bool EditingControlValueChanged
{
get
{
return valueChanged;
}
set
{
valueChanged = value;
}
}
// Implements the IDataGridViewEditingControl
// .EditingPanelCursor property.
public Cursor EditingPanelCursor
{
get
{
return base.Cursor;
}
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
if (valueChanged)
{
base.OnSelectedIndexChanged(e);
}
}
protected override void OnSelectedValueChanged(EventArgs e)
{
if (valueChanged)
{
base.OnSelectedValueChanged(e);
}
}
protected override void OnSelectedItemChanged(EventArgs e)
{
if (valueChanged)
{
base.OnSelectedItemChanged(e);
}
}
}
#endregion

Method addObserver must be called on the main thread Exception, While inserting data to room database

I am trying to insert data into the room database using the kotlin coroutine. But I always get an exception java.lang.IllegalStateException: Method addObserver must be called on the main thread
But I don't have an observer in this code, the insert call is called from launch with Dispatchers IO
DocumentDao.kt
#Dao
interface DocumentDao {
#Insert
suspend fun insertDocument(document: Document): Long
}
Repository.kt
class Repository#Inject constructor(val db: MyDB) {
suspend fun demoInsert(
uri: String,
albumId: Long
): Long {
val newDoc = Document(0, albumId, rawUri = uri)
return db.documentDao().insertDocument(newDoc)
}
}
MyViewModel.kt
#HiltViewModel
class MyViewModel#Inject constructor(val repo: Repository) : ViewModel() {
suspend fun demoInsert(
uri: String,
albumId: Long
): Long {
return repo.demoInsert(uri, albumId)
}
}
MyFrag.kt
#AndroidEntryPoint
class MyFrag: Fragment() {
val viewModel: MyViewModel by viewModels()
....
....
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.insert.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
val res = viewModel.demoInsert("test", Random.nextLong(500))
Log.d(TAG, "onViewCreated: $res")
}
}
........
.......
}
}
what is wrong with this code? please help me
I'm not sure about this but you can launch coroutine inside listener with Main Dispatcher and later use withContext inside DB function, to change context.
I was facing the same issue and I solved yhis way :
private fun insertAllItemsInDb(data : List<PostResponse>){
val listPost = data.map { it.toUI() }
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
localViewModel.insertAllPosts(listPost)
}
}
ViewModel:
fun insertAllPosts(posts: List<PostItem>) {
viewModelScope.launch {
dbRepository.insertAllPosts(posts)
}
}
Creating view model with:
val viewModel: MyViewModel by viewModels()
Will result in lazy creating. Creation of real object will be performed when you access your object for first time. This happens inside:
lifecycleScope.launch(Dispatchers.IO) {
val res = viewModel.demoInsert("test", Random.nextLong(500))
Log.d(TAG, "onViewCreated: $res")
}
And since implementation of method viewModels<>() looks like this:
#MainThread
public inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)
You are getting
Method addObserver must be called on the main thread
You should be able to fix this with something like this.
lifecycleScope.launch(Dispatchers.IO) {
val res = withContext(Dispatchers.Main + lifecycleScope.coroutineContext){}.demoInsert("test", Random.nextLong(500))
Log.d(TAG, "onViewCreated: $res")
}
MyViewModel.kt
#HiltViewModel
class MyViewModel#Inject constructor(val repo: Repository) : ViewModel() {
suspend fun demoInsert(
uri: String,
albumId: Long
): Long {
viewModelScope.launch {
repo.demoInsert(uri, albumId)
}
}
}
MyFrag.kt
#AndroidEntryPoint
class MyFrag: Fragment() {
val viewModel: MyViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.insert.setOnClickListener {
lifecycleScope.launch(Dispatchers.Main) {
viewModel.demoInsert("test", Random.nextLong(500))
}
}
}
}

Spring reactor parallel flux is stuck

I am using reactor to create an infinite flux,
once I make it parallel, the stream gets stuck after the first passed value, can't figure out why
val source = source().parallel().runOn(Schedulers.parallel())
.map(this::toUpperCase)
.subscribe(sink())
private fun sink() = SimpleSink<SimpleDaoModel>()
private fun toUpperCase(simpleDaoModel: SimpleDaoModel) = simpleDaoModel.copy(stringValue = simpleDaoModel.stringValue.toUpperCase())
private fun source() = Flux.create { sink: FluxSink<SimpleDaoModel> ->
fun getNextAsync(): Job = GlobalScope.launch(Dispatchers.Default) {
val task = customSimpleModelRepository.getNextTask()
if (task != null) {
logger.info("emitting next task")
sink.next(task)
} else {
logger.info("No more tasks")
Timer("nextTaskBackoff", false).schedule(1000) {
getNextAsync()
}
}
}
sink.onRequest { getNextAsync() }
}
class SimpleSink<T> : BaseSubscriber<T>() {
public override fun hookOnSubscribe(subscription: Subscription) {
println("Subscribed")
request(1)
}
public override fun hookOnNext(value: T) {
println(value)
request(1)
}
}
If I remove the parallel operator, everything works like a charm.
Note: getNextTask is a suspended function

Issue getting fault message from message context in Masstransit

I have an application that needs to intercept the current message consume context and extract a value that is defined in a base interface. That value is a tenant code that is eventually used in an EF database context.
I have a provider that takes a MassTransit ConsumerContext, and then using context.TryGetMessage(), extracts the tenant code, which is ultimately used to switch database contexts to a specific tenant database.
The issue lies in the MessageContextTenantProvider below. If a non-fault message is consumed then ConsumeContext<IBaseEvent> works fine. However if it is a fault, ConsumeContext<Fault<IBaseEvent>> doesn't work as expected.
Durring debugging I can see that the message context for a fault is ConsumeContext<Fault<IVerifyEvent>>, but why doesn't it work with a base interface as per the standard message? Of course, ConsumeContext<Fault<IVerifiedEvent>> works fine, but I have a lot of message types, and I don't want to have to define them all in that tenant provider.
Any ideas?
public interface ITenantProvider
{
string GetTenantCode();
}
public class MessageContextTenantProvider : ITenantProvider
{
private readonly ConsumeContext _consumeContext;
public MessageContextTenantProvider(ConsumeContext consumeContext)
{
_consumeContext = consumeContext;
}
public string GetTenantCode()
{
// get tenant from message context
if (_consumeContext.TryGetMessage(out ConsumeContext<IBaseEvent> baseEvent))
{
return baseEvent.Message.TenantCode; // <-- works for the non fault consumers
}
// get tenant from fault message context
if (_consumeContext.TryGetMessage<Fault<IBaseEvent>>(out var gebericFaultEvent))
{
return gebericFaultEvent.Message.Message.TenantCode; // <- doesn't work generically
}
// get tenant from fault message context (same as above)
if (_consumeContext.TryGetMessage(out ConsumeContext<Fault<IBaseEvent>> faultEvent))
{
return faultEvent.Message.Message.TenantCode; // <= generically doesn't work when using the base interface?
}
// get tenant from specific concrete fault class
if (_consumeContext.TryGetMessage(out ConsumeContext<Fault<IVerifiedEvent>> verifiedFaultEvent))
{
return verifiedFaultEvent.Message.Message.TenantCode; // <-- this works
}
// not able to extract tenant
return null;
}
}
public partial class VerificationDbContext
{
string connectionString;
public string ConnectionString
{
get
{
if (connectionString == null)
{
string tenantCode = _tenantProvider.GetTenantCode();
connectionString = _tenantConnectionManager.GetConnectionString(orgId);
}
return connectionString;
}
}
private readonly ITenantProvider _tenantProvider;
private readonly ITenantConnectionManager _tenantConnectionManager;
public VerificationDbContext(ITenantProvider tenantProvider, ITenantConnectionManager tenantConnectionManager)
{
_tenantProvider = tenantProvider;
_tenantConnectionManager = tenantConnectionManager;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (string.IsNullOrEmpty(this.ConnectionString))
{
optionsBuilder.UseSqlServer(#"Data Source=.\SQLEXPRESS;Initial Catalog=VerificationDb;Integrated Security=True")
.ConfigureWarnings((warningBuilder) => warningBuilder.Ignore(RelationalEventId.AmbientTransactionWarning));
}
else
{
optionsBuilder.UseSqlServer(this.ConnectionString)
.ConfigureWarnings((warningBuilder) => warningBuilder.Ignore(RelationalEventId.AmbientTransactionWarning));
}
}
}
public interface ITenantConnectionManager
{
string GetConnectionString(string tenantCode);
}
public class TenantConnectionManager : ITenantConnectionManager
{
private ITenantRepository _tenantRepository;
public TenantConnectionManager(ITenantRepository tenantRepository)
{
_tenantRepository = tenantRepository;
}
public string GetConnectionString(string tenantCode)
{
return _tenantRepository.GetByTenantCode(tenantCode).ConnectionString;
}
}
public interface IBaseEvent
{
string TenantCode { get; }
}
public interface IVerifiedEvent : IBaseEvent
{
string JobReference { get; }
}
public class VerifiedEventConsumer : IConsumer<IVerifiedEvent>
{
private readonly IVerifyCommand _verifyCommand;
private readonly ITenantProvider _tenantProvider;
public VerifiedEventConsumer(ITenantProvider tenantProvider, IVerifyCommand verifyCommand)
{
_verifyCommand = verifyCommand;
_tenantProvider = tenantProvider;
}
public async Task Consume(ConsumeContext<IVerifiedEvent> context)
{
await _verifyCommand.Execute(new VerifyRequest
{
JobReference = context.Message.JobReference,
TenantCode = context.Message.TenantCode
});
}
}
public class VerifiedEventFaultConsumer : IConsumer<Fault<IVerifiedEvent>>
{
private readonly IVerifyFaultCommand _verifyFaultCommand;
private readonly ITenantProvider _tenantProvider;
public CaseVerifiedEventFaultConsumer(ITenantProvider tenantProvider, IVerifyFaultCommand verifyFaultCommand)
{
_verifyFaultCommand = verifyFaultCommand;
_tenantProvider = tenantProvider;
}
public async Task Consume(ConsumeContext<Fault<ICaseVerifiedEvent>> context)
{
await _verifyFaultCommand.Execute(new VerifiedFaultRequest
{
JobReference = context.Message.Message.JobReference,
Exceptions = context.Message.Exceptions
});
}
}
I've solved the issue by using the GreenPipes TryGetPayload extension method:
public class MessageContextTenantProvider : ITenantProvider
{
private readonly ConsumeContext _consumeContext;
public MessageContextTenantProvider(ConsumeContext consumeContext)
{
_consumeContext = consumeContext;
}
public string GetTenantCode()
{
// get tenant from message context
if (_consumeContext.TryGetMessage(out ConsumeContext<IBaseEvent> baseEvent))
{
return baseEvent.Message.TenantCode;
}
// get account code from fault message context using Greenpipes
if (_consumeContext.TryGetPayload(out ConsumeContext<Fault<IBaseEvent>> payloadFaultEvent))
{
return payloadFaultEvent.Message.Message.TenantCode;
}
// not able to extract tenant
return null;
}
}

Resources