How to add Flutter MethodChannel to Android Application.class without creating a flutter plugin?
I can do it in Activity, but somehow I cannot access MethodChannels if I add them in Application.class.
Android:
Logs: MissingPluginException(No implementation found for method getPreferences on channel ...)
class App : FlutterApplication(), PluginRegistry.PluginRegistrantCallback {
override fun onCreate() {
super.onCreate()
setPreferencesChannel()
}
override fun registerWith(reg: PluginRegistry?) {
GeneratedPluginRegistrant.registerWith(reg)
}
private fun setPreferencesChannel() {
val channel = MethodChannel(FlutterEngine(this).dartExecutor.binaryMessenger, applicationContext.packageName + "/preferences")
channel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
when (call.method) {
"getPreferences" -> {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val map = HashMap<String, Any?>()
map["font_size"] = prefs.getInt("font_size_main", 0)
result.success(map)
}
else -> result.notImplemented()
}
}
}}
Flutter:
class PreferencesChannel {
static final _channel =
MethodChannel(BuildConfig.packageName + '/preferences');
PreferencesChannel._();
static Future<dynamic> getPreferences() async {
try {
return _channel.invokeMethod('getPreferences');
} on PlatformException catch (e) {
Logger.logError(e.message, 'PreferencesChannel: getPreferences');
return null;
}
}
}
Related
In MainActivity.kt, I invoke a method call "goToSecondActivity" to go to second activity. I would like to invoke method channel on the SecondActivity too but it doesn't work. The MethodCallHandler doesn't even run.
I got the MissingPluginException(No implementation found for method updatePosition on channel com.example.ble_poc/map_channel) but I have just implemented it and channel name is the same.
Can anyone tell me what should I do? I am new to Android Native.
MainActivtiy.kt:
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.ble_poc/channel"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegister.registerGeneratedPlugins(FlutterEngine(this#MainActivity))
MethodChannel(flutterEngine?.dartExecutor!!, CHANNEL).setMethodCallHandler { call, result ->
if(call.method.equals("goToSecondActivity")){
goToSecondActivity()
} else {
print("Result not implemented in MainActivity")
result.notImplemented()
}
}
}
private fun goToSecondActivity() {
startActivity(Intent(this#MainActivity, SecondActivity::class.java))
}
}
SecondActivity.kt:
class SecondActivity: FlutterActivity() {
private val CHANNEL = "com.example.ble_poc/map_channel"
private lateinit var mapView: MPIMapView
private val venueDataString: String by lazy { readFileContentFromAssets("mappedin-demo-mall.json") }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegister.registerGeneratedPlugins(FlutterEngine(this#SecondActivity))
MethodChannel(flutterEngine?.dartExecutor!!.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if(call.method.equals("updatePosition")){
println("Update position call is triggered")
val lat = call.argument<Double>("lat")
val long = call.argument<Double>("long")
val accuracy = call.argument<Double>("accuracy") ?: 7.170562529369844
val floorLevel = call.argument<Int>("floor") ?: 0
updatePosition(lat!!, long!!, accuracy, floorLevel)
} else {
print("Result not implemented in SecondActivity")
result.notImplemented()
}
}
...
main.dart
static const CHANNEL = "com.example.ble_poc/channel";
static const MAP_CHANNEL = "com.example.ble_poc/map_channel";
static const platform = MethodChannel(CHANNEL);
static const map_platform = MethodChannel(MAP_CHANNEL);
void openMap() async {
try {
platform.invokeMethod('goToSecondActivity');
Timer(const Duration(seconds: 4), () {
map_platform.invokeMethod(
"updatePosition",
{
"lat": 43.52023014,
"long": -80.5352595,
},
);
});
} on PlatformException catch (e) {
print(e.message);
}
}
...
I am using msal-flutter in one of the flutter app.
environment:
sdk: ">=2.7.0 <3.0.0"
flutter: ">=1.10.0"
Getting following error in Android app.
E/MethodChannel#msal_flutter(28234): Failed to handle method call
E/MethodChannel#msal_flutter(28234): kotlin.UninitializedPropertyAccessException: lateinit property mainActivity has not been initialized
E/MethodChannel#msal_flutter(28234): at com.signify.msal_flutter.MsalFlutterPlugin.initialize(MsalFlutterPlugin.kt:230)
E/MethodChannel#msal_flutter(28234): at com.signify.msal_flutter.MsalFlutterPlugin.onMethodCall(MsalFlutterPlugin.kt:125)
E/MethodChannel#msal_flutter(28234): at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:233)
E/MethodChannel#msal_flutter(28234): at io.flutter.embedding.engine.dart.DartMessenger.handleMessageFromDart(DartMessenger.java:85)
E/MethodChannel#msal_flutter(28234): at io.flutter.embedding.engine.FlutterJNI.handlePlatformMessage(FlutterJNI.java:692)
E/MethodChannel#msal_flutter(28234): at android.os.MessageQueue.nativePollOnce(Native Method)
E/MethodChannel#msal_flutter(28234): at android.os.MessageQueue.next(MessageQueue.java:335)
E/MethodChannel#msal_flutter(28234): at android.os.Looper.loop(Looper.java:183)
E/MethodChannel#msal_flutter(28234): at android.app.ActivityThread.main(ActivityThread.java:7660)
E/MethodChannel#msal_flutter(28234): at java.lang.reflect.Method.invoke(Native Method)
E/MethodChannel#msal_flutter(28234): at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
E/MethodChannel#msal_flutter(28234): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
I guess method registerWith is not calling.
Following is the code using in MsalFlutterPlugin class:
#Suppress("SpellCheckingInspection")
public class MsalFlutterPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel : MethodChannel
override fun onAttachedToEngine(#NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "msal_flutter")
channel.setMethodCallHandler(this);
}
companion object {
lateinit var mainActivity : Activity
lateinit var msalApp: IMultipleAccountPublicClientApplication
fun isClientInitialized() = ::msalApp.isInitialized
#JvmStatic
fun registerWith(registrar: Registrar) {
Log.d("MsalFlutter","Registering plugin")
val channel = MethodChannel(registrar.messenger(), "msal_flutter")
channel.setMethodCallHandler(MsalFlutterPlugin())
mainActivity = registrar.activity()
}
fun getAuthCallback(result: Result) : AuthenticationCallback
{
Log.d("MsalFlutter", "Getting the auth callback object")
return object : AuthenticationCallback
{
override fun onSuccess(authenticationResult : IAuthenticationResult){
Log.d("MsalFlutter", "Authentication successful")
Handler(Looper.getMainLooper()).post {
result.success(authenticationResult.accessToken)
}
}
override fun onError(exception : MsalException)
{
Log.d("MsalFlutter","Error logging in!")
Log.d("MsalFlutter", exception.message)
Handler(Looper.getMainLooper()).post {
result.error("AUTH_ERROR", "Authentication failed", exception.localizedMessage)
}
}
override fun onCancel(){
Log.d("MsalFlutter", "Cancelled")
Handler(Looper.getMainLooper()).post {
result.error("CANCELLED", "User cancelled", null)
}
}
}
}
private fun getApplicationCreatedListener(result: Result) : IPublicClientApplication.ApplicationCreatedListener {
Log.d("MsalFlutter", "Getting the created listener")
return object : IPublicClientApplication.ApplicationCreatedListener
{
override fun onCreated(application: IPublicClientApplication) {
Log.d("MsalFlutter", "Created successfully")
msalApp = application as MultipleAccountPublicClientApplication
result.success(true)
}
override fun onError(exception: MsalException?) {
Log.d("MsalFlutter", "Initialize error")
result.error("INIT_ERROR", "Error initializting client", exception?.localizedMessage)
}
}
}
}
override fun onMethodCall(#NonNull call: MethodCall, #NonNull result: Result) {
val scopesArg : ArrayList<String>? = call.argument("scopes")
val scopes: Array<String>? = scopesArg?.toTypedArray()
val clientId : String? = call.argument("clientID")
val authority : String? = call.argument("authorityURL")
Log.d("A_MsalFlutter","Got scopes: ${scopes.toString()}")
Log.d("A_MsalFlutter","Got cleintId: $clientId")
Log.d("A_MsalFlutter","Got authority: $authority")
when(call.method){
"logout" -> Thread(Runnable{logout(result)}).start()
"init" -> initialize(clientId, authority, result)
"acquireTokenInteractively" -> Thread(Runnable {acquireToken(scopes, result)}).start()
"acquireTokenSilent" -> Thread(Runnable {acquireTokenSilent(scopes, result)}).start()
else -> result.notImplemented()
}
}
private fun acquireToken(scopes : Array<String>?, result: Result)
{
Log.d("MsalFlutter", "acquire token called")
// check if client has been initialized
if(!isClientInitialized()){
Log.d("MsalFlutter","Client has not been initialized")
Handler(Looper.getMainLooper()).post {
result.error("NO_CLIENT", "Client must be initialized before attempting to acquire a token.", null)
}
}
//check scopes
if(scopes == null){
Log.d("MsalFlutter", "no scope")
result.error("NO_SCOPE", "Call must include a scope", null)
return
}
//remove old accounts
while(msalApp.accounts.any()){
Log.d("MsalFlutter","Removing old account")
msalApp.removeAccount(msalApp.accounts.first())
}
//acquire the token
msalApp.acquireToken(mainActivity, scopes, getAuthCallback(result))
}
private fun acquireTokenSilent(scopes : Array?, result: Result)
{
Log.d("MsalFlutter", "Called acquire token silent")
// check if client has been initialized
if(!isClientInitialized()){
Log.d("MsalFlutter","Client has not been initialized")
Handler(Looper.getMainLooper()).post {
result.error("NO_CLIENT", "Client must be initialized before attempting to acquire a token.", null)
}
}
//check the scopes
if(scopes == null){
Log.d("MsalFlutter", "no scope")
Handler(Looper.getMainLooper()).post {
result.error("NO_SCOPE", "Call must include a scope", null)
}
return
}
//ensure accounts exist
if(msalApp.accounts.isEmpty()){
Handler(Looper.getMainLooper()).post {
result.error("NO_ACCOUNT", "No account is available to acquire token silently for", null)
}
return
}
//acquire the token and return the result
val res = msalApp.acquireTokenSilent(scopes, msalApp.accounts[0], msalApp.configuration.defaultAuthority.authorityURL.toString())
Handler(Looper.getMainLooper()).post {
result.success(res.accessToken)
}
}
private fun initialize(clientId: String?, authority: String?, result: Result)
{
Log.d("MsalFlutter", "inside the initialize block")
//ensure clientid provided
if(clientId == null){
Log.d("MsalFlutter","error no clientId")
result.error("NO_CLIENTID", "Call must include a clientId", null)
return
}
//if already initialized, ensure clientid hasn't changed
if(isClientInitialized()){
Log.d("MsalFlutter","Client already initialized.")
if(msalApp.configuration.clientId == clientId)
{
result.success(true)
} else {
result.error("CHANGED_CLIENTID", "Attempting to initialize with multiple clientIds.", null)
}
}
// if authority is set, create client using it, otherwise use default
if(authority != null){
Log.d("MsalFlutter", "Authority not null")
Log.d("MsalFlutter", "Creating with: $clientId - $authority")
PublicClientApplication.create(mainActivity.applicationContext, clientId, authority, getApplicationCreatedListener(result))
}else{
Log.d("MsalFlutter", "Authority null")
PublicClientApplication.create(mainActivity.applicationContext, clientId, getApplicationCreatedListener(result))
}
}
private fun logout(result: Result){
while(msalApp.accounts.any()){
Log.d("MsalFlutter","Removing old account")
msalApp.removeAccount(msalApp.accounts.first())
}
Handler(Looper.getMainLooper()).post {
result.success(true)
}
}
override fun onDetachedFromEngine(#NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
msal_auth.dart::
class MsalAuth {
static const MethodChannel _channel =
const MethodChannel('msal_flutter');
//Configuration parameters
String _clientId, _authority, _redirectURI;
MsalAuth(String clientID, {String authorityURL, String redirectURL}){
throw Exception("Direct call is not allowed. Please use PublicClientApplication static method");
}
MsalAuth._create(String clientId, {String authority, String redirectURI}) {
_clientId = clientId;
_authority = authority;
_redirectURI = redirectURI;
}
static Future publicClientApplication(String clientID, {String authorityURL, String redirectURL}) async{
var res = MsalAuth._create(clientID, authority: authorityURL, redirectURI: redirectURL);
await res._initialize();
return res;
}
Future _initialize() async {
var res = <String, dynamic>{'clientID': this._clientId};
//if authority has been set, add it aswell
if (this._authority != null) {
res["authorityURL"] = this._authority;
}
if (this._redirectURI != null) {
res["redirectURI"] = this._redirectURI;
}
try {
await _channel.invokeMethod('init', res);
} on PlatformException catch (e) {
throw _convertException(e);
}
}
/// Acquire a token interactively for the given [scopes]
Future acquireToken(List scopes) async {
var res = <String, dynamic>{'scopes': scopes};
try {
final String token = await _channel.invokeMethod('acquireTokenInteractively', res);
return token;
} on PlatformException catch (e) {
throw _convertException(e);
}
}
/// Acquire a token silently, with no user interaction, for the given [scopes]
Future acquireTokenSilent(List scopes) async {
var res = <String, dynamic>{'scopes': scopes};
try {
final String token =
await _channel.invokeMethod('acquireTokenSilently', res);
return token;
} on PlatformException catch (e) {
throw _convertException(e);
}
}
Future logout() async {
try {
await _channel.invokeMethod('logout', <String, dynamic>{});
} on PlatformException catch (e) {
throw _convertException(e);
}
}
MsalException _convertException(PlatformException e) {
switch (e.code) {
case "CANCELLED":
return MsalUserCancelledException();
case "NO_SCOPE":
return MsalInvalidScopeException();
case "NO_ACCOUNT":
return MsalNoAccountException();
case "NO_CLIENTID":
return MsalInvalidConfigurationException("Client Id not set");
case "INVALID_AUTHORITY":
return MsalInvalidConfigurationException("Invalid authroity set.");
case "CONFIG_ERROR":
return MsalInvalidConfigurationException(
"Invalid configuration, please correct your settings and try again");
case "NO_CLIENT":
return MsalUninitializedException();
case "CHANGED_CLIENTID":
return MsalChangedClientIdException();
case "INIT_ERROR":
return MsalInitializationException();
case "AUTH_ERROR":
default:
return MsalException("Authentication error");
}
}
}
I was following along a tutorial described in this article.
The code of the article can be found here: https://github.com/seamusv/event_channel_sample.
I basically do the same only that i use kotlin instead of java.
In native code (MainActivity.kt):
class MainActivity: FlutterActivity() {
private val STREAM_TAG = "alarm.eventchannel.sample/stream";
private var timerSubscription : Disposable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
EventChannel(getFlutterView(), STREAM_TAG).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
Log.w("TAG", "adding listener")
this#MainActivity.timerSubscription = Observable.interval(0, 1, TimeUnit.SECONDS)
.subscribe (
{
Log.w("Test", "Result we just received: $it");
events.success(1);
}, // OnSuccess
{ error -> events.error("STREAM", "Error in processing observable", error); }, // OnError
{ println("Complete"); } // OnCompletion
)
}
override fun onCancel(arguments: Any?) {
Log.w("TAG", "adding listener")
if (this#MainActivity.timerSubscription != null) {
this#MainActivity.timerSubscription?.dispose()
this#MainActivity.timerSubscription = null
}
}
}
)
}
}
In my main.dart i do the following:
int _timer = 0;
StreamSubscription _timerSubscription = null;
void _enableTimer() {
if (_timerSubscription == null) {
_timerSubscription = stream.receiveBroadcastStream().listen(_updateTimer);
}
}
void _disableTimer() {
if (_timerSubscription != null) {
_timerSubscription.cancel();
_timerSubscription = null;
}
}
void _updateTimer(timer) {
debugPrint("Timer $timer");
setState(() => _timer = timer);
}
In the build function i also create a button which then calls _enableTimer() onPressed.
new FlatButton(
child: const Text('Enable'),
onPressed: _enableTimer,
)
Whenever i now press the button to call _enableTimer() the app crashes and i get the output "Lost connection to device"...
Am i doing something wrong or is this a bug in a newer version of Flutter since the article is from December 2017?
The solution to my problem was basically to start the stream in the main thread:
class MainActivity: FlutterActivity() {
private val CHANNEL = "alarm.flutter.dev/audio"
private val STREAM_TAG = "alarm.eventchannel.sample/stream";
private var timerSubscription : Disposable? = null
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
EventChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), STREAM_TAG).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
Log.w("TAG", "adding listener")
this#MainActivity.timerSubscription = Observable
.interval(1000, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe (
{
Log.w("Test", "Result we just received: $it");
events.success(it);
}, // OnSuccess
{ error -> events.error("STREAM", "Error in processing observable", error); }, // OnError
{ println("Complete"); } // OnCompletion
)
}
override fun onCancel(arguments: Any?) {
Log.w("TAG", "adding listener")
if (this#MainActivity.timerSubscription != null) {
this#MainActivity.timerSubscription?.dispose()
this#MainActivity.timerSubscription = null
}
}
}
)
}
I've just finished my first Android App. It works as it should but, as you can imagine, there's a lot of spaghetti code and lack of performance. From what I've learned on Android and Kotlin language making this project (and a lot of articles/tutorials/SO answers) I'm trying to start it again from scratch to realize a better version. For now I'd like to keep it as simple as possible, just to better understand how to handle API calls with Retrofit and MVVM pattern, so no Volley/RXjava/Dagger etc.
I'm starting from the login obviously; I would like to make a post request to simply compare the credentials, wait for the response and, if positive, show a "loading screen" while fetching and processing data to show in the home page. I'm not storing any information so I have realized a singleton class that holds data as long as the app is running (btw, is there another way to do it?).
RetrofitService
private val retrofitService = Retrofit.Builder()
.addConverterFactory(
GsonConverterFactory
.create(
GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.setLenient().setDateFormat("yyyy-MM-dd")
.create()
)
)
.addConverterFactory(RetrofitConverter.create())
.baseUrl(BASE_URL)
.build()
`object ApiObject {
val retrofitService: ApiInterface by lazy {
retrofitBuilder.create(ApiInterface::class.java) }
}
ApiInterface
interface ApiInterface {
#GET("workstation/{date}")
suspend fun getWorkstations(
#Path("date") date: Date
): List<Workstation>
#GET("reservation/{date}")
suspend fun getReservations(
#Path("date") date: Date
): List<Reservation>
#GET("user")
suspend fun getUsers(): List<User>
#GET("user/login")
suspend fun validateLoginCredentials(
#Query("username") username: String,
#Query("password") password: String
): Response<User>
ApiResponse
sealed class ApiResponse<T> {
companion object {
fun <T> create(response: Response<T>): ApiResponse<T> {
return if(response.isSuccessful) {
val body = response.body()
// Empty body
if (body == null || response.code() == 204) {
ApiSuccessEmptyResponse()
} else {
ApiSuccessResponse(body)
}
} else {
val msg = response.errorBody()?.string()
val errorMessage = if(msg.isNullOrEmpty()) {
response.message()
} else {
msg.let {
return#let JSONObject(it).getString("message")
}
}
ApiErrorResponse(errorMessage ?: "Unknown error")
}
}
}
}
class ApiSuccessResponse<T>(val data: T): ApiResponse<T>()
class ApiSuccessEmptyResponse<T>: ApiResponse<T>()
class ApiErrorResponse<T>(val errorMessage: String): ApiResponse<T>()
Repository
class Repository {
companion object {
private var instance: Repository? = null
fun getInstance(): Repository {
if (instance == null)
instance = Repository()
return instance!!
}
}
private var singletonClass = SingletonClass.getInstance()
suspend fun validateLoginCredentials(username: String, password: String) {
withContext(Dispatchers.IO) {
val result: Response<User>?
try {
result = ApiObject.retrofitService.validateLoginCredentials(username, password)
when (val response = ApiResponse.create(result)) {
is ApiSuccessResponse -> {
singletonClass.loggedUser = response.data
}
is ApiSuccessEmptyResponse -> throw Exception("Something went wrong")
is ApiErrorResponse -> throw Exception(response.errorMessage)
}
} catch (error: Exception) {
throw error
}
}
}
suspend fun getWorkstationsListFromService(date: Date) {
withContext(Dispatchers.IO) {
val workstationsListResult: List<Workstation>
try {
workstationsListResult = ApiObject.retrofitService.getWorkstations(date)
singletonClass.rWorkstationsList.postValue(workstationsListResult)
} catch (error: Exception) {
throw error
}
}
}
suspend fun getReservationsListFromService(date: Date) {
withContext(Dispatchers.IO) {
val reservationsListResult: List<Reservation>
try {
reservationsListResult = ApiObject.retrofitService.getReservations(date)
singletonClass.rReservationsList.postValue(reservationsListResult)
} catch (error: Exception) {
throw error
}
}
}
suspend fun getUsersListFromService() {
withContext(Dispatchers.IO) {
val usersListResult: List<User>
try {
usersListResult = ApiObject.retrofitService.getUsers()
singletonClass.rUsersList.postValue(usersListResult.let { usersList ->
usersList.filterNot { user -> user.username == "admin" }
.sortedWith(Comparator { x, y -> x.surname.compareTo(y.surname) })
})
} catch (error: Exception) {
throw error
}
}
}
SingletonClass
const val FAILED = 0
const val COMPLETED = 1
const val RUNNING = 2
class SingletonClass private constructor() {
companion object {
private var instance: SingletonClass? = null
fun getInstance(): SingletonClass {
if (instance == null)
instance = SingletonClass()
return instance!!
}
}
//User
var loggedUser: User? = null
//Workstations List
val rWorkstationsList = MutableLiveData<List<Workstation>>()
//Reservations List
val rReservationsList = MutableLiveData<List<Reservation>>()
//Users List
val rUsersList = MutableLiveData<List<User>>()
}
ViewModel
class ViewModel : ViewModel() {
private val singletonClass = SingletonClass.getInstance()
private val repository = Repository.getInstance()
//MutableLiveData
//Login
private val _loadingStatus = MutableLiveData<Boolean>()
val loadingStatus: LiveData<Boolean>
get() = _loadingStatus
private val _successfulAuthenticationStatus = MutableLiveData<Boolean>()
val successfulAuthenticationStatus: LiveData<Boolean>
get() = _successfulAuthenticationStatus
//Data fetch
private val _listsLoadingStatus = MutableLiveData<Int>()
val listsLoadingStatus: LiveData<Int>
get() = _listsLoadingStatus
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String>
get() = _errorMessage
fun onLoginClicked(username: String, password: String) {
launchLoginAuthentication {
repository.validateLoginCredentials(username, password)
}
}
private fun launchLoginAuthentication(block: suspend () -> Unit): Job {
return viewModelScope.launch {
try {
_loadingStatus.value = true
block()
} catch (error: Exception) {
_errorMessage.postValue(error.message)
} finally {
_loadingStatus.value = false
if (singletonClass.loggedUser != null)
_successfulAuthenticationStatus.value = true
}
}
}
fun onLoginPerformed() {
val date = Calendar.getInstance().time
launchListsFetch {
//how to start these all at the same time? Then wait until their competion
//and call the two methods below?
repository.getReservationsListFromService(date)
repository.getWorkstationsListFromService(date)
repository.getUsersListFromService()
}
}
private fun launchListsFetch(block: suspend () -> Unit): Job {
return viewModelScope.async {
try {
_listsLoadingStatus.value = RUNNING
block()
} catch (error: Exception) {
_listsLoadingStatus.value = FAILED
_errorMessage.postValue(error.message)
} finally {
//I'd like to perform these operations at the same time
prepareWorkstationsList()
prepareReservationsList()
//and, when both completed, set this value
_listsLoadingStatus.value = COMPLETED
}
}
}
fun onToastShown() {
_errorMessage.value = null
}
}
LoginActivity
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel
get() = ViewModelProviders.of(this).get(LoginViewModel::class.java)
private val loadingFragment = LoadingDialogFragment()
var username = ""
var password = ""
private lateinit var loginButton: Button
lateinit var context: Context
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
loginButton = findViewById(R.id.login_button)
loginButton.setOnClickListener {
username = login_username.text.toString().trim()
password = login_password.text.toString().trim()
viewModel.onLoginClicked(username, password.toMD5())
}
viewModel.loadingStatus.observe(this, Observer { value ->
value?.let { show ->
progress_bar_login.visibility = if (show) View.VISIBLE else View.GONE
}
})
viewModel.successfulAuthenticationStatus.observe(this, Observer { successfullyLogged ->
successfullyLogged?.let {
loadingFragment.setStyle(DialogFragment.STYLE_NORMAL, R.style.CustomLoadingDialogFragment)
if (successfullyLogged) {
loadingFragment.show(supportFragmentManager, "loadingFragment")
viewModel.onLoginPerformed()
} else {
login_password.text.clear()
login_password.isFocused
password = ""
}
}
})
viewModel.listsLoadingStatus.observe(this, Observer { loadingResult ->
loadingResult?.let {
when (loadingResult) {
COMPLETED -> {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
setResult(Activity.RESULT_OK)
finish()
}
FAILED -> {
loadingFragment.changeText("Error")
loadingFragment.showProgressBar(false)
loadingFragment.showRetryButton(true)
}
}
}
})
viewModel.errorMessage.observe(this, Observer { value ->
value?.let { message ->
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
viewModel.onToastShown()
}
})
}
Basically what I'm trying to do is to send username and password, show a progress bar while waiting for the result (if successful the logged user object is returned, otherwise a toast with the error message is shown), hide the progress bar and show the loading fragment. While showing the loading fragment start 3 async network calls and wait for their completion; when the third call is completed start the methods to elaborate the data and, when both done, start the next activity.
It seems to all works just fine, but debugging I've noticed the flow (basically network calls start/wait/onCompletion) is not at all like what I've described above. There's something to fix in the ViewModel, I guess, but I can't figure out what
I'm trying to learn how to use RxJava in Android, but have run into a dead end. I have the following DataSource:
object DataSource {
enum class FetchStyle {
FETCH_SUCCESS,
FETCH_EMPTY,
FETCH_ERROR
}
var relay: BehaviorRelay<FetchStyle> = BehaviorRelay.createDefault(FetchStyle.FETCH_ERROR)
fun fetchData(): Observable<DataModel> {
return relay
.map { f -> loadData(f) }
}
private fun loadData(f: FetchStyle): DataModel {
Thread.sleep(5000)
return when (f) {
FetchStyle.FETCH_SUCCESS -> DataModel("Data Loaded")
FetchStyle.FETCH_EMPTY -> DataModel(null)
FetchStyle.FETCH_ERROR -> throw IllegalStateException("Error Fetching")
}
}
}
I want to trigger an update downstream, whenever I change the value of relay, but this doesn't happen. It works when the Activity is initialized, but not when I'm updating the value. Here's my ViewModel, from where I update the value:
class MainViewModel : ViewModel() {
val fetcher: Observable<UiStateModel> = DataSource.fetchData().replay(1).autoConnect()
.map { result -> UiStateModel.from(result) }
.onErrorReturn { exception -> UiStateModel.Error(exception) }
.startWith(UiStateModel.Loading())
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
fun loadSuccess() {
DataSource.relay.accept(DataSource.FetchStyle.FETCH_SUCCESS)
}
fun loadEmpty() {
DataSource.relay.accept(DataSource.FetchStyle.FETCH_EMPTY)
}
fun loadError() {
DataSource.relay.accept(DataSource.FetchStyle.FETCH_ERROR)
}
}
This is the code from the Activity that does the subsciption:
model.fetcher
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
uiState -> mainPresenter.loadView(uiState)
})
Ended up using kotlin coroutines instead, as I was unable to re-subscribe to ConnectableObservable and start a new fetch.
Here's the code for anyone interested.
The presenter:
class MainPresenter(val view: MainView) {
private lateinit var subscription: SubscriptionReceiveChannel<UiStateModel>
fun loadSuccess(model: MainViewModel) {
model.loadStyle(DataSource.FetchStyle.FETCH_SUCCESS)
}
fun loadError(model: MainViewModel) {
model.loadStyle(DataSource.FetchStyle.FETCH_ERROR)
}
fun loadEmpty(model: MainViewModel) {
model.loadStyle(DataSource.FetchStyle.FETCH_EMPTY)
}
suspend fun subscribe(model: MainViewModel) {
subscription = model.connect()
subscription.subscribe { loadView(it) }
}
private fun loadView(uiState: UiStateModel) {
when(uiState) {
is Loading -> view.isLoading()
is Error -> view.isError(uiState.exception.localizedMessage)
is Success -> when {
uiState.result != null -> view.isSuccess(uiState.result)
else -> view.isEmpty()
}
}
}
fun unSubscribe() {
subscription.close()
}
}
inline suspend fun <E> SubscriptionReceiveChannel<E>.subscribe(action: (E) -> Unit) = consumeEach { action(it) }
The view:
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
launch(UI) {
mainPresenter.subscribe(model)
}
btn_load_success.setOnClickListener {
mainPresenter.loadSuccess(model)
}
btn_load_error.setOnClickListener {
mainPresenter.loadError(model)
}
btn_load_empty.setOnClickListener {
mainPresenter.loadEmpty(model)
}
}
override fun onDestroy() {
super.onDestroy()
Log.d("View", "onDestroy()")
mainPresenter.unSubscribe()
}
...
The model:
class MainViewModel : ViewModel() {
val TAG = this.javaClass.simpleName
private val stateChangeChannel = ConflatedBroadcastChannel<UiStateModel>()
init {
/** When the model is initialized we immediately start fetching data */
fetchData()
}
override fun onCleared() {
super.onCleared()
Log.d(TAG, "onCleared() called")
stateChangeChannel.close()
}
fun connect(): SubscriptionReceiveChannel<UiStateModel> {
return stateChangeChannel.openSubscription()
}
fun fetchData() = async {
stateChangeChannel.send(UiStateModel.Loading())
try {
val state = DataSource.loadData().await()
stateChangeChannel.send(UiStateModel.from(state))
} catch (e: Exception) {
Log.e("MainModel", "Exception happened when sending new state to channel: ${e.cause}")
}
}
internal fun loadStyle(style: DataSource.FetchStyle) {
DataSource.style = style
fetchData()
}
}
And here's a link to the project on github.