I'm trying to port Zendesk Native SDK for Android in Flutter using MethodChannel and Kotlin as my language choice.
It works when I use the Kotlin code directly inside the project which is
class MainActivity : FlutterActivity() {
// others code are hidden
private fun initialize(call: MethodCall, result: Result) {
val url: String = call.argument("url")!!
val appId: String = call.argument("appId")!!
val clientId: String = call.argument("clientId")!!
Zendesk.INSTANCE.init(this, url, appId, clientId)
val identity = AnonymousIdentity()
Zendesk.INSTANCE.setIdentity(identity)
Support.INSTANCE.init(Zendesk.INSTANCE)
RequestListActivity.builder().show(this)
result.success(true)
}
}
The this is referring to Activity which I guess FlutterApplication already has inside of it, but when I try to make the standalone plugin thing is a little bit different. I need to implement ActivityAware to get the activity (Get activity reference in flutter plugin).
https://github.com/flutter/flutter/wiki/Experimental:-Create-Flutter-Plugin
(Optional) If your plugin needs an Activity reference, also implement ActivityAware.
public class ZendeskPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private lateinit var activityBinding: ActivityPluginBinding
// I can get ActivityPluginBinding from this method
override fun onAttachedToActivity(#NonNull binding: ActivityPluginBinding) {
activityBinding = binding
}
private fun initialize(call: MethodCall, result: Result) {
activityBinding?.activity?.let {
val appId: String = call.argument("appId")!!
val clientId: String = call.argument("clientId")!!
val url: String = call.argument("url")!!
Zendesk.INSTANCE.init(it, url, appId, clientId)
val identity = AnonymousIdentity()
Zendesk.INSTANCE.setIdentity(identity)
Support.INSTANCE.init(Zendesk.INSTANCE)
RequestListActivity.builder().show(it)
result.success(true)
return
}
result.error("INITIALIZE_FAILED", "Failed to initialize", null)
}
}
I tried to call initialize from dart and actually it runs but onAttachedToActivity seems is never be invoked and makes activityBinding is never be initialized so the code fails and result.error.
How do I get activity inside FlutterPlugin class?
Thank you
Probably the problem is the call of the setMethodCallHandler in the onAttachedToEngine
Look more here
Related
I have an existing Flutter app that I use to prototype and test ideas before adding the idea to a project. I need a custom plugin to track location in the background for a project so I added plugin related code into the normal app project.
I am targeting android for a start. I have a Dart class representing the plugin that creates a method a channel to communicate with the platform code. On the platform side, I have created a class that extends FlutterPlugin
However, when I run the app and Dart native code calls methods on the Android side using the method channel, I get Unhandled Exception: MissingPluginException.
Here's the code
Dart code
class GeofencePlugin {
final MethodChannel _channel =
const MethodChannel('marcel/geofencing_plugin');
Future<bool> init() async {
//callbackDispatcher is a top level function that acts as entry point for background isolate
final callback = PluginUtilities.getCallbackHandle(callbackDispatcher);
await _channel
.invokeMethod('GeofencingPlugin.initialiseService', <dynamic>[callback!.toRawHandle()]);
return true;
}
Future<bool> registerGeofence(GeofenceRegion region) async {
return true;
}
Future<bool> removeGeofence(GeofenceRegion region) async {
return true;
}
}
Android code
class GeofencingPlugin : ActivityAware, FlutterPlugin, MethodChannel.MethodCallHandler {
private var mContext : Context? = null
private var mActivity : Activity? = null
private val geofencePendingIntent: PendingIntent by lazy {
val intent = Intent(mContext, GeofenceBroadcastReceiver::class.java)
PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
companion object {
#JvmStatic
private val TAG = "MARK_TAG"
#JvmStatic
val SHARED_PREFERENCES_KEY = "com.example.flutter_playground.geofencing"
#JvmStatic
val CALLBACK_DISPATCHER_HANDLE_KEY = "callback_dispatch_handler"
#JvmStatic
private fun initialiseService(context: Context, args: ArrayList<*>?) {
val callbackHandle = args!![0] as Long
context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
.edit()
.putLong(CALLBACK_DISPATCHER_HANDLE_KEY, callbackHandle)
.commit()
}
}
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
mContext = binding.applicationContext
val channel = MethodChannel(binding.binaryMessenger, "marcel/geofencing_plugin")
channel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
mContext = null
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
mActivity = binding.activity
}
override fun onDetachedFromActivity() {
mActivity = null
}
override fun onDetachedFromActivityForConfigChanges() {
mActivity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
mActivity = binding.activity
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
val args = call.arguments<ArrayList<*>>()
when(call.method) {
"GeofencingPlugin.initialiseService" -> {
initialiseService(mContext!!, args)
setupGeo()
result.success(true)
}
else -> result.notImplemented()
}
}
private fun setupGeo(){
val geofencingClient = mContext!!.getGeofenceClient()
val fence = Geofence.Builder()
.setRequestId("Mark")
.setCircularRegion(46.5422,14.4011,500f)
.setExpirationDuration(600000)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
.build()
val request = GeofencingRequest.Builder().apply {
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_DWELL)
addGeofences(listOf(fence))
}.build()
geofencingClient.addGeofences(request, geofencePendingIntent)?.run {
addOnSuccessListener {
Toast.makeText(mContext,"Hahaha", Toast.LENGTH_LONG).show()
}
addOnFailureListener {
Log.d(TAG,it.message?:"Eoo")
Toast.makeText(mContext,"Heeerrrh", Toast.LENGTH_LONG).show()
}
}
}
}
I tried the following
Manually adding the android plugin class to the GeneratedPluginRegistrant.java file
Using flutterEngine.plugins.add('my plugin') in the MainActivity's onCreate method.
After digging into the pubsec.yaml of other plugins, I found out in order to have my app's plugin recognised by flutter I have to add the following to my pubsec.yaml under the flutter section:
plugin:
platforms:
android:
package: com.example.flutter_playground.geofencing
pluginClass: GeofencingPlugin
I created Flutter plugin. But I also need to add more channels. So I created both dart code and the native implementation. The problem is that the dart code cannot find the native implementation, so it throw out error with the message "Unhandled exception: MissingPluginException (No implementation found for method ...)"
I compared my native implementation with the boilerplate implementation, I don't see anything wrong. I have the correct channel name. I have registered the channel. My channel name is certainly different from the builderplate implementation's channel name. But it matches the one in dart code.
I wonder if I need to have all my native implementations to have the same channel name even though they represent different functionality blocks?
You can create multiple MethodChannels for your plugin and assign an individual MethodCallHandler to each MethodChannel. That way you can handle two methods with the same name differently if they are in different channels.
Here is how I've changed the Flutter plugin template to support multiple MethodChannels.
P.S.: I've used the Flutter 2.10.5 plugin template because it's a bit easier to understand. In Flutter 3 they've added an interface on the Dart side.
Dart:
class ExamplePlugin {
static const MethodChannel _channel = MethodChannel('example_plugin');
static const MethodChannel _channel2 = MethodChannel('example_plugin2');
static Future<String?> get platformVersion async {
final String? version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
static Future<String?> get helloWorld async {
final String? helloWorld = await _channel2.invokeMethod('getHelloWorld');
return helloWorld;
}
}
Kotlin:
class ExamplePlugin: FlutterPlugin {
private lateinit var channel : MethodChannel
private lateinit var channel2 : MethodChannel
private val firstMethodCallHandler = FirstMethodCallHandler()
private val secondMethodCallHandler = SecondMethodCallHandler()
override fun onAttachedToEngine(#NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "example_plugin")
channel.setMethodCallHandler(firstMethodCallHandler)
channel2 = MethodChannel(flutterPluginBinding.binaryMessenger, "example_plugin2")
channel2.setMethodCallHandler(secondMethodCallHandler)
}
override fun onDetachedFromEngine(#NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
channel2.setMethodCallHandler(null)
}
private inner class FirstMethodCallHandler: MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
}
private inner class SecondMethodCallHandler: MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getHelloWorld") {
result.success("Hello World!")
} else {
result.notImplemented()
}
}
}
}
Swift:
public class SwiftExamplePlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "example_plugin", binaryMessenger: registrar.messenger())
let channel2 = FlutterMethodChannel(name: "example_plugin2", binaryMessenger: registrar.messenger())
channel.setMethodCallHandler(firstMethodCallHandler)
channel2.setMethodCallHandler(secondMethodCallHandler)
}
static public func firstMethodCallHandler(_ call: FlutterMethodCall, result: #escaping FlutterResult) {
result("iOS " + UIDevice.current.systemVersion)
}
static public func secondMethodCallHandler(_ call: FlutterMethodCall, result: #escaping FlutterResult) {
result("Hello World!")
}
}
Complete source code: https://github.com/ColinSchmale/example_plugin
I am trying to use JWT in my API, and configuration is completed, can use postman tool to access data from it. However when I use Blazor as front end to access it , the request doesn't have token, so always give a 401 code.
Below is my Blazor code.
program.cs
builder.Services.AddHttpClient<IOptionService, OptionService> ("OptionAPI", (sp, cl) => {
cl.BaseAddress = new Uri("https://localhost:7172");
});
builder.Services.AddScoped(
sp => sp.GetService<IHttpClientFactory>().CreateClient("OptionAPI"));
OptionService.cs
public class OptionService : IOptionService {
private readonly HttpClient _httpClient;
public OptionService(HttpClient httpClient) {
_httpClient = httpClient;
}
public async Task<IEnumerable<OptionOutputDto>> GetOptionsAsync(Guid quizId, Guid questionId) {
_httpClient.DefaultRequestHeaders.Authorization
= new AuthenticationHeaderValue("Bearer", "token");
return await JsonSerializer.DeserializeAsync<IEnumerable<OptionOutputDto>>(
await _httpClient.GetStreamAsync($"api/quizzes/{quizId}/{questionId}/options"),
new JsonSerializerOptions {
PropertyNameCaseInsensitive = true
});
}
I tired use " new AuthenticationHeaderValue("Bearer", "token");" to attach token in header, but its not working, still give 401 code.
And I also tried use
private readonly IHttpClientFactory _httpClient;
public OptionService(IHttpClientFactory httpClient) {
_httpClient = httpClient;
}
public async Task<IEnumerable<OptionOutputDto>> GetOptionsAsync(Guid quizId, Guid questionId) {
var newHttpClient = _httpClient.CreateClient();
newHttpClient.DefaultRequestHeaders.Authorization
= new AuthenticationHeaderValue("Bearer", "token");
return await JsonSerializer.DeserializeAsync<IEnumerable<OptionOutputDto>>(
await newHttpClient.GetStreamAsync($"api/quizzes/{quizId}/{questionId}/options"),
new JsonSerializerOptions {
PropertyNameCaseInsensitive = true
});
}
it's also not working, give me an error,
Unhandled exception rendering component: A suitable constructor for type 'Services.OptionService' could not be located. Ensure the type is concrete and all parameters of a public constructor are either registered as services or passed as arguments. Also ensure no extraneous arguments are provided.
System.InvalidOperationException: A suitable constructor for type .....
Can anyone has a simple way to attach token in request header?
Thanks in advance.
I think the good option is :
builder.Services.AddHttpClient<IOptionService, OptionService> ("OptionAPI", (sp, cl) => {
cl.BaseAddress = new Uri("https://localhost:7172");
});
Could you check if the token is present in header or not?
Your error is most likely related to how the OptionService is being registered in dependency injection. It either needs an empty constructor adding - and/or - you need to ensure that the constructor has all of its dependencies registered correctly in the ServicesCollection too.
The exception is quite explicit:
Ensure the type is concrete and all parameters of a public constructor
are either registered as services or passed as arguments. Also ensure
no extraneous arguments are provided
I gave a similar answer here. Basically you need to include the BaseAddressAuthorizationMessageHandler when defining your httpclients. If you're using a typed httpclient, you can inject the IAccessTokenProvider and get the token from there. Kinda like this:
public class MyHttpClient(IAccessTokenProvider tokenProvider, HttpClient httpClient)
{
_tokenProvider = tokenProvider;
_httpClient = httpClient;
}
private async Task RequestAuthToken()
{
var requestToken = await _tokenProvider.RequestAccessToken();
requestToken.TryGetToken(out var token);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Value);
}
public async Task<IEnumerable<ReplyDto>> SendHttpRequest()
{
await RequestAuthToken();
return await JsonSerializer.DeserializeAsync<IEnumerable<ReplyDto>>(
await _httpClient.GetStreamAsync("api/getendpoint"),
new JsonSerializerOptions {
PropertyNameCaseInsensitive = true
});
}
This question is a follow up to this problem here: How to make retrofit API call using ViewModel and LiveData
The mistakes 1 and 2 highlighted in that post's response have been fixed. For mistake 3, I haven't yet moved the API call to a repository, but I will once the code start working properly.
So I'm trying to make an API call using Retrofit, using MVVM with LiveData and ViewModel. The API call (which currently is in the ViewModel), is working properly, but the changes is not being picked up by the Observer in the Activity.
I've setup my ViewModel observer as follow:
public class PopularGamesActivity extends AppCompatActivity {
private static final String igdbBaseUrl = "https://api-endpoint.igdb.com/";
private static final String FIELDS = "id,name,genres,cover,popularity";
private static final String ORDER = "popularity:desc";
private static final int LIMIT = 30;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_popular_games);
PopularGamesViewModel popViewModel = ViewModelProviders.of(this).get(PopularGamesViewModel.class);
popViewModel.getGameList().observe(this, new Observer<List<Game>>() {
#Override
public void onChanged(#Nullable List<Game> gameList) {
String firstName = gameList.get(0).getName();
Timber.d(firstName);
}
And my ViewModel code is as follow:
public class PopularGamesViewModel extends AndroidViewModel {
private static final String igdbBaseUrl = "https://api-endpoint.igdb.com/";
private static final String FIELDS = "id,name,genres,cover,popularity";
private static final String ORDER = "popularity:desc";
private static final int LIMIT = 30;
public PopularGamesViewModel(#NonNull Application application) {
super(application);
}
public LiveData<List<Game>> getGameList() {
final MutableLiveData<List<Game>> gameList = new MutableLiveData<>();
// Create the retrofit builder
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(igdbBaseUrl)
.addConverterFactory(GsonConverterFactory.create());
// Build retrofit
Retrofit retrofit = builder.build();
// Create the retrofit client
RetrofitClient client = retrofit.create(RetrofitClient.class);
Call<List<Game>> call = client.getGame(FIELDS,
ORDER,
LIMIT);
call.enqueue(new Callback<List<Game>>() {
#Override
public void onResponse(Call<List<Game>> call, Response<List<Game>> response) {
Timber.d("api call sucesss");
if (response.body() != null) {
Timber.d("First game: " + response.body().get(0).getName());
gameList.setValue(response.body());
}
}
#Override
public void onFailure(Call<List<Game>> call, Throwable t) {
Timber.d("api call failed");
}
});
return gameList;
}
}
When I run the code, the onResponse in the ViewModel class will output the correct response from the API call, so the call is working properly. But the onChanged() in the PopularGamesActivity class will never get called. Can someone shed some light on what I'm doing wrong? Thank you!
Ok, so this turned out to be a weird android studio bug. I was initially running the code on my real nexus 4 device, and the onChange never gets called. However, after running it on an emulated device, it started working immediately. And now, it's working on my real device too.
I don't know the actual reason behind it, but if anyone in the future run into a problem where onChange won't get called, try switching device/emulators.
Cheers.
I debug the code on a device and have the same problem.
M'n solution was simply activate the device screen.
The screensaver was the problem..
Hi I'm trying to unit test my logout action on my controller but I have hard times to test or stub my Session in the HttpContext. I'm using MVC Contrib TestHelper to make it easier but now I need a little help.
Here's my test :
[TestFixture]
public class SessionControllerTest
{
private ISession _session;
private IConfigHelper _configHelper;
private IAuthenticationService _authService;
//private IMailHelper _mailHelper;
private ICryptographer _crypto;
private SessionController _controller;
private TestControllerBuilder _builder;
private MockRepository _mock;
[SetUp]
public void Setup()
{
_mock = new MockRepository();
_session = _mock.DynamicMock<ISession>();
_configHelper = _mock.DynamicMock<IConfigHelper>();
_authService = _mock.DynamicMock<IAuthenticationService>();
//_mailHelper = _mock.DynamicMock<IMailHelper>();
_crypto = _mock.DynamicMock<ICryptographer>();
_controller = new SessionController(_authService, _session, _crypto, _configHelper);
_builder = new TestControllerBuilder();
_builder.InitializeController(_controller);
}
[Test]
public void Logout_ReturnRedirectToAction()
{
_builder.InitializeController(_controller);
_authService.SignOut();
LastCall.Repeat.Once();
_builder.Session["memberNumber"] = string.Empty;
LastCall.Repeat.Once();
_controller.Session.Clear();
LastCall.Repeat.Any();
_controller.Session.Abandon();
LastCall.Repeat.Any();
//_builder.Session.Stub(s => s.Clear());
//_builder.Session.Stub(s => s.Abandon());
//_builder.Session.Clear();
//LastCall.Repeat.Once();
//_builder.Session.Abandon();
//LastCall.Repeat.Once();
_mock.ReplayAll();
var result = _controller.Logout();
_mock.VerifyAll();
result.AssertActionRedirect().ToAction<SessionController>(c => c.Login());
}
You can see my differents attemps. I get an error telling me that Session.Abandon() is not implemented, witch is right when you take a look at MVCContrib's TestHelper. But how can I mock or Stub the Session that's already mocked by the TestHelper?
The Exception in NUnit :
System.NotImplementedException : The
method or operation is not
implemented. at
MvcContrib.TestHelper.MockSession.Abandon()
Thank you for the help!
EDIT : Here's the new working test
[Test]
public void Logout_ReturnRedirectToAction()
{
_builder.InitializeController(_controller);
var mockSession = _mock.Stub<HttpSessionStateBase>();
_controller.HttpContext.BackToRecord();
_controller.HttpContext.Stub(c => c.Session).Return(mockSession);
_controller.HttpContext.Replay();
_authService.SignOut();
LastCall.Repeat.Once();
_builder.Session["memberNumber"] = string.Empty;
_controller.Session.Clear();
LastCall.Repeat.Once();
_controller.Session.Abandon();
LastCall.Repeat.Once();
_mock.ReplayAll();
var result = _controller.Logout();
_mock.VerifyAll();
result.AssertActionRedirect().ToAction<SessionController>(c => c.Login());
}
It's been a while since I used MvcContrib, so I pulled down the latest code and made a quick test project. It's very odd. Looking at the MvcContrib code (specifically, TestControllerBuilder), it creates mocks for most of the objects (request, response, server, etc...), but not for Session. I'm not sure why this is -- probably have to ask the creators.
However, there is a way to mock it yourself. You can create your own mock session and tell the controller to use yours instead of the one from MvcContrib.TestHelpers. Here's what I did in my test:
var mockSession = MockRepository.GenerateStub<HttpSessionStateBase>();
controller.HttpContext.BackToRecord();
controller.HttpContext.Stub(c => c.Session).Return(mockSession);
controller.HttpContext.Replay();
Now I run my controller method and then use Rhino.Mocks' AAA syntax for making sure the Abandon method was called:
controller.Session.AssertWasCalled(s => s.Abandon());
If you want to use record/replay semantics, you could set your expectations before calling controller.HttpContext.Replay().