From bc7cb9edd25a86224ffc314abb8d2d9a58e1bd33 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Tue, 21 Jul 2026 12:44:00 +0900 Subject: [PATCH 1/9] [flutter_inappwebview] Fix onTitleChanged to fire after the initial page load The Tizen implementation only reported the page title once, right after a page finished loading. It never listened for the WebView's own title-changed notifications, so title updates made afterwards (for example by JavaScript setting document.title) were never reported to onTitleChanged. Register a "title,changed" listener on the underlying webview instance, matching the pattern already used for load and navigation events, so onTitleChanged fires whenever the title actually changes. --- .../flutter_inappwebview/tizen/src/webview.cc | 16 ++++++++++++++++ .../flutter_inappwebview/tizen/src/webview.h | 1 + 2 files changed, 17 insertions(+) diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc index 323947569..def626cd9 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.cc +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -354,6 +354,8 @@ void WebView::Dispose() { &WebView::OnNavigationPolicy); evas_object_smart_callback_del(webview_instance_, "url,changed", &WebView::OnUrlChange); + evas_object_smart_callback_del(webview_instance_, "title,changed", + &WebView::OnTitleChange); auto& ewk_view = EwkInternalApiBinding::GetInstance().view; if (ewk_view.OnJavaScriptAlert) { ewk_view.OnJavaScriptAlert(webview_instance_, nullptr, nullptr); @@ -620,6 +622,8 @@ bool WebView::InitWebView() { &WebView::OnNavigationPolicy, this); evas_object_smart_callback_add(webview_instance_, "url,changed", &WebView::OnUrlChange, this); + evas_object_smart_callback_add(webview_instance_, "title,changed", + &WebView::OnTitleChange, this); Resize(width_, height_); evas_object_show(webview_instance_); @@ -1112,6 +1116,18 @@ void WebView::OnUrlChange(void* data, Evas_Object* obj, void* event_info) { std::make_unique(args)); } +void WebView::OnTitleChange(void* data, Evas_Object* obj, void* event_info) { + WebView* webview = static_cast(data); + const char* title = static_cast(event_info); + if (!title) { + return; + } + flutter::EncodableMap args = { + {flutter::EncodableValue("title"), flutter::EncodableValue(title)}}; + webview->webview_channel_->InvokeMethod( + "onTitleChanged", std::make_unique(args)); +} + void WebView::OnEvaluateJavaScript(Evas_Object* obj, const char* result_value, void* user_data) { FlMethodResult* result = static_cast(user_data); diff --git a/packages/flutter_inappwebview/tizen/src/webview.h b/packages/flutter_inappwebview/tizen/src/webview.h index cf42b38f0..b17925b8a 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.h +++ b/packages/flutter_inappwebview/tizen/src/webview.h @@ -92,6 +92,7 @@ class WebView : public PlatformView { static void OnNavigationPolicy(void* data, Evas_Object* obj, void* event_info); static void OnUrlChange(void* data, Evas_Object* obj, void* event_info); + static void OnTitleChange(void* data, Evas_Object* obj, void* event_info); static void OnEvaluateJavaScript(Evas_Object* obj, const char* result_value, void* user_data); static Eina_Bool OnJavaScriptAlertDialog(Evas_Object* o, const char* message, From cfd02b5d2b76eb1e0ba45636641d56f4fd2b5179 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Wed, 12 Aug 2026 18:26:45 +0900 Subject: [PATCH 2/9] [flutter_inappwebview] Fix getUrl race and shouldOverrideUrlLoading round-trip on programmatic navigation OnNavigationPolicy always suspended the view and asked Dart's shouldOverrideUrlLoading whether to allow a navigation, even for navigations the app itself requested (loadUrl, goBack, reload, ...). That round-trip is meant for user/page-initiated navigation only. Also, when Dart calls stopLoading() to cancel a pending navigation, getUrl() had no way to know a cancellation happened: EWK's "url,changed" event can still fire for the cancelled URL (before or after ewk_view_stop() takes effect), so getUrl() could end up reporting a URL the app never actually finished navigating to. Fix both: - Every EWK call that starts an app-requested navigation now goes through NavigateProgrammatically(), which marks the navigation as programmatic. OnNavigationPolicy checks this flag and accepts immediately, skipping the shouldOverrideUrlLoading round-trip for it. - StopNavigation() records that the current navigation was cancelled and reverts committed_url_ to the URL snapshotted just before the navigation decision was accepted (pending_navigation_revert_url_). OnUrlChange ignores "url,changed" while a cancellation is pending, and getUrl() returns committed_url_ instead of asking EWK directly in that window. --- .../flutter_inappwebview/tizen/src/webview.cc | 130 +++++++++++++++--- .../flutter_inappwebview/tizen/src/webview.h | 13 ++ 2 files changed, 121 insertions(+), 22 deletions(-) diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc index def626cd9..2c4b7da5b 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.cc +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -307,9 +307,23 @@ void WebView::StopNavigation() { if (disposed_ || !webview_instance_) { return; } + is_navigation_cancelled_ = true; + if (!pending_navigation_revert_url_.empty()) { + committed_url_ = pending_navigation_revert_url_; + } + ewk_view_resume(webview_instance_); ewk_view_stop(webview_instance_); } +bool WebView::NavigateProgrammatically(const std::function& ewk_call) { + is_programmatic_navigation_ = true; + const bool started = ewk_call(); + if (!started) { + is_programmatic_navigation_ = false; + } + return started; +} + void WebView::Dispose() { if (disposed_) { return; @@ -491,7 +505,10 @@ bool WebView::SendKey(const char* key, const char* string, const char* compose, if (strcmp(key, "XF86Back") == 0 && !is_down) { if (ewk_view_back_possible(webview_instance_)) { - ewk_view_back(webview_instance_); + NavigateProgrammatically([this] { + ewk_view_back(webview_instance_); + return true; + }); return true; } return false; @@ -689,7 +706,10 @@ void WebView::ApplyInitialParams(const flutter::EncodableValue& params) { std::string url = std::string("file://") + res_path + "flutter_assets/" + initial_file; free(res_path); - ewk_view_url_set(webview_instance_, url.c_str()); + NavigateProgrammatically([this, &url] { + ewk_view_url_set(webview_instance_, url.c_str()); + return true; + }); return; } } @@ -701,8 +721,11 @@ void WebView::ApplyInitialParams(const flutter::EncodableValue& params) { std::string base_url = "about:blank"; if (GetValueFromEncodableMap(initial_data, "data", &data)) { GetValueFromEncodableMap(initial_data, "baseUrl", &base_url); - ewk_view_html_string_load(webview_instance_, data.c_str(), - base_url.c_str(), nullptr); + NavigateProgrammatically([this, &data, &base_url] { + ewk_view_html_string_load(webview_instance_, data.c_str(), + base_url.c_str(), nullptr); + return true; + }); return; } } @@ -712,7 +735,10 @@ void WebView::ApplyInitialParams(const flutter::EncodableValue& params) { &url_request)) { std::string url; if (GetValueFromEncodableMap(url_request, "url", &url) && !url.empty()) { - ewk_view_url_set(webview_instance_, url.c_str()); + NavigateProgrammatically([this, &url] { + ewk_view_url_set(webview_instance_, url.c_str()); + return true; + }); } } } @@ -772,16 +798,22 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, } const auto ewk_method = method == "POST" ? EWK_HTTP_METHOD_POST : EWK_HTTP_METHOD_GET; - bool ret = ewk_view_url_request_set( - webview_instance_, url.c_str(), ewk_method, ewk_headers, - body.empty() ? nullptr : reinterpret_cast(body.data())); + const bool ret = NavigateProgrammatically([&] { + return ewk_view_url_request_set( + webview_instance_, url.c_str(), ewk_method, ewk_headers, + body.empty() ? nullptr + : reinterpret_cast(body.data())); + }); eina_hash_free(ewk_headers); if (!ret) { result->Error("Operation failed", "Failed to load URL request."); return; } } else { - ewk_view_url_set(webview_instance_, url.c_str()); + NavigateProgrammatically([this, &url] { + ewk_view_url_set(webview_instance_, url.c_str()); + return true; + }); } result->Success(); } else if (method_name == "postUrl") { @@ -795,9 +827,12 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, if (!body.empty()) { body.push_back('\0'); } - const bool ret = ewk_view_url_request_set( - webview_instance_, url.c_str(), EWK_HTTP_METHOD_POST, nullptr, - body.empty() ? nullptr : reinterpret_cast(body.data())); + const bool ret = NavigateProgrammatically([&] { + return ewk_view_url_request_set( + webview_instance_, url.c_str(), EWK_HTTP_METHOD_POST, nullptr, + body.empty() ? nullptr + : reinterpret_cast(body.data())); + }); if (ret) { result->Success(); } else { @@ -810,8 +845,11 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, return; } GetValueFromEncodableMap(arguments, "baseUrl", &base_url); - ewk_view_html_string_load(webview_instance_, data.c_str(), base_url.c_str(), - nullptr); + NavigateProgrammatically([this, &data, &base_url] { + ewk_view_html_string_load(webview_instance_, data.c_str(), + base_url.c_str(), nullptr); + return true; + }); result->Success(); } else if (method_name == "loadFile") { std::string file_path; @@ -831,7 +869,10 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, url = std::string("file://") + res_path + "flutter_assets/" + file_path; free(res_path); } - ewk_view_url_set(webview_instance_, url.c_str()); + NavigateProgrammatically([this, &url] { + ewk_view_url_set(webview_instance_, url.c_str()); + return true; + }); result->Success(); } else if (method_name == "canGoBack") { result->Success(flutter::EncodableValue( @@ -840,18 +881,31 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, result->Success(flutter::EncodableValue( static_cast(ewk_view_forward_possible(webview_instance_)))); } else if (method_name == "goBack") { - ewk_view_back(webview_instance_); + NavigateProgrammatically([this] { + ewk_view_back(webview_instance_); + return true; + }); result->Success(); } else if (method_name == "goForward") { - ewk_view_forward(webview_instance_); + NavigateProgrammatically([this] { + ewk_view_forward(webview_instance_); + return true; + }); result->Success(); } else if (method_name == "reload") { - ewk_view_reload(webview_instance_); + NavigateProgrammatically([this] { + ewk_view_reload(webview_instance_); + return true; + }); result->Success(); } else if (method_name == "getUrl") { - const char* url = ewk_view_url_get(webview_instance_); - result->Success(url ? flutter::EncodableValue(url) - : flutter::EncodableValue()); + if (is_navigation_cancelled_ && !committed_url_.empty()) { + result->Success(flutter::EncodableValue(committed_url_)); + } else { + const char* url = ewk_view_url_get(webview_instance_); + result->Success(url ? flutter::EncodableValue(url) + : flutter::EncodableValue()); + } } else if (method_name == "getTitle") { const char* title = ewk_view_title_get(webview_instance_); result->Success(title ? flutter::EncodableValue(std::string(title)) @@ -1007,6 +1061,7 @@ void WebView::OnFrameRendered(void* data, Evas_Object* obj, void* event_info) { void WebView::OnLoadStarted(void* data, Evas_Object* obj, void* event_info) { WebView* webview = static_cast(data); + webview->is_programmatic_navigation_ = false; flutter::EncodableMap args = { {flutter::EncodableValue("url"), flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}}; @@ -1016,6 +1071,7 @@ void WebView::OnLoadStarted(void* data, Evas_Object* obj, void* event_info) { void WebView::OnLoadFinished(void* data, Evas_Object* obj, void* event_info) { WebView* webview = static_cast(data); + webview->is_programmatic_navigation_ = false; flutter::EncodableMap args = { {flutter::EncodableValue("url"), flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}}; @@ -1044,6 +1100,7 @@ void WebView::OnProgress(void* data, Evas_Object* obj, void* event_info) { void WebView::OnLoadError(void* data, Evas_Object* obj, void* event_info) { WebView* webview = static_cast(data); + webview->is_programmatic_navigation_ = false; Ewk_Error* error = static_cast(event_info); std::string url = ewk_error_url_get(error) ? std::string(ewk_error_url_get(error)) : ""; @@ -1084,6 +1141,26 @@ void WebView::OnNavigationPolicy(void* data, Evas_Object* obj, WebView* webview = static_cast(data); Ewk_Policy_Decision* policy_decision = static_cast(event_info); + + // A new navigation decision means any previous cancellation is stale: + // getUrl() should stop overriding with the old committed_url_ snapshot. + webview->is_navigation_cancelled_ = false; + + if (webview->is_programmatic_navigation_) { + webview->is_programmatic_navigation_ = false; + ewk_policy_decision_use(policy_decision); + return; + } + + // Snapshot the URL EWK is displaying before accepting the navigation + // below. EWK can fire "url,changed" for the new (possibly-to-be-cancelled) + // URL as soon as ewk_policy_decision_use() runs, racing with the async + // shouldOverrideUrlLoading round-trip. StopNavigation() reverts getUrl() + // using this snapshot rather than whatever "url,changed" reported last, so + // that race can't leave getUrl() stuck on a cancelled URL. + const std::string url_before_navigation = + GetViewUrl(webview->webview_instance_); + // Always accept the navigation on its original frame so iframe loads stay // in their iframe. The view is then suspended while we wait for the Dart // shouldOverrideUrlLoading response and either resumed (allow) or stopped @@ -1093,6 +1170,8 @@ void WebView::OnNavigationPolicy(void* data, Evas_Object* obj, return; } + webview->pending_navigation_revert_url_ = url_before_navigation; + const char* url_cstr = ewk_policy_decision_url_get(policy_decision); const std::string url = url_cstr ? std::string(url_cstr) : std::string(); ewk_view_suspend(webview->webview_instance_); @@ -1107,9 +1186,16 @@ void WebView::OnNavigationPolicy(void* data, Evas_Object* obj, void WebView::OnUrlChange(void* data, Evas_Object* obj, void* event_info) { WebView* webview = static_cast(data); + if (webview->is_navigation_cancelled_) { + // Stale "url,changed" for the navigation we just cancelled (EWK can fire + // it before or after ewk_view_stop() takes effect); getUrl() is already + // pinned to committed_url_ and must not be overwritten with this URL. + return; + } + webview->committed_url_ = GetViewUrl(webview->webview_instance_); flutter::EncodableMap args = { {flutter::EncodableValue("url"), - flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}, + flutter::EncodableValue(webview->committed_url_)}, {flutter::EncodableValue("isReload"), flutter::EncodableValue(false)}}; webview->webview_channel_->InvokeMethod( "onUpdateVisitedHistory", diff --git a/packages/flutter_inappwebview/tizen/src/webview.h b/packages/flutter_inappwebview/tizen/src/webview.h index b17925b8a..96f913389 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.h +++ b/packages/flutter_inappwebview/tizen/src/webview.h @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -83,6 +84,14 @@ class WebView : public PlatformView { bool InitWebView(); + // Runs an EWK call that starts a navigation the app itself requested + // (loadUrl, goBack, reload, ...), marking it as programmatic first so the + // next OnNavigationPolicy skips the shouldOverrideUrlLoading round-trip for + // it. If `ewk_call` reports the navigation never started, the flag is + // cleared immediately instead of leaking into some later, unrelated + // navigation. Returns whatever `ewk_call` returned. + bool NavigateProgrammatically(const std::function& ewk_call); + static void OnFrameRendered(void* data, Evas_Object* obj, void* event_info); static void OnLoadStarted(void* data, Evas_Object* obj, void* event_info); static void OnLoadFinished(void* data, Evas_Object* obj, void* event_info); @@ -128,6 +137,10 @@ class WebView : public PlatformView { bool texture_registered_ = false; bool disposed_ = false; Ewk_Mouse_Button_Type mouse_button_type_ = (Ewk_Mouse_Button_Type)0; + bool is_programmatic_navigation_ = false; + bool is_navigation_cancelled_ = false; + std::string committed_url_; + std::string pending_navigation_revert_url_; static std::set instances_; static std::mutex instances_mutex_; From 48067560805dded0bb73b95f4f59e4730707486a Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Wed, 12 Aug 2026 18:30:54 +0900 Subject: [PATCH 3/9] [flutter_inappwebview] Fix scrollBy/getScrollX/getScrollY returning stale position ewk_view_scroll_pos_get() right after ewk_view_scroll_set() can return the pre-scroll position because EWK applies the scroll asynchronously, so scrollBy's delta and getScrollX/getScrollY's return value were sometimes stale by one frame. Track the last requested scroll position in target_scroll_x_/y_ and use it as the source of truth until EWK's reported position catches up with it, then fall back to querying EWK directly. Reset both to -1 on navigation start/error since a new page invalidates any pending scroll target. --- .../flutter_inappwebview/tizen/src/webview.cc | 32 +++++++++++++++++-- .../flutter_inappwebview/tizen/src/webview.h | 2 ++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc index 2c4b7da5b..4430baa34 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.cc +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -944,11 +944,19 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, } if (method_name == "scrollTo") { ewk_view_scroll_set(webview_instance_, x, y); + target_scroll_x_ = x; + target_scroll_y_ = y; } else { - ewk_view_scroll_by(webview_instance_, x, y); + int32_t current_x = 0, current_y = 0; + ewk_view_scroll_pos_get(webview_instance_, ¤t_x, ¤t_y); + int32_t base_x = (target_scroll_x_ >= 0) ? target_scroll_x_ : current_x; + int32_t base_y = (target_scroll_y_ >= 0) ? target_scroll_y_ : current_y; + target_scroll_x_ = base_x + x; + target_scroll_y_ = base_y + y; + ewk_view_scroll_set(webview_instance_, target_scroll_x_, target_scroll_y_); } - int32_t new_x = 0, new_y = 0; - ewk_view_scroll_pos_get(webview_instance_, &new_x, &new_y); + int32_t new_x = target_scroll_x_; + int32_t new_y = target_scroll_y_; flutter::EncodableMap args = { {flutter::EncodableValue("x"), flutter::EncodableValue(new_x)}, {flutter::EncodableValue("y"), flutter::EncodableValue(new_y)}, @@ -959,6 +967,20 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, } else if (method_name == "getScrollX" || method_name == "getScrollY") { int32_t x = 0, y = 0; ewk_view_scroll_pos_get(webview_instance_, &x, &y); + if (target_scroll_x_ >= 0) { + if (x == target_scroll_x_) { + target_scroll_x_ = -1; + } else { + x = target_scroll_x_; + } + } + if (target_scroll_y_ >= 0) { + if (y == target_scroll_y_) { + target_scroll_y_ = -1; + } else { + y = target_scroll_y_; + } + } result->Success( flutter::EncodableValue(method_name == "getScrollX" ? x : y)); } else if (method_name == "zoomBy") { @@ -1062,6 +1084,8 @@ void WebView::OnFrameRendered(void* data, Evas_Object* obj, void* event_info) { void WebView::OnLoadStarted(void* data, Evas_Object* obj, void* event_info) { WebView* webview = static_cast(data); webview->is_programmatic_navigation_ = false; + webview->target_scroll_x_ = -1; + webview->target_scroll_y_ = -1; flutter::EncodableMap args = { {flutter::EncodableValue("url"), flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}}; @@ -1101,6 +1125,8 @@ void WebView::OnProgress(void* data, Evas_Object* obj, void* event_info) { void WebView::OnLoadError(void* data, Evas_Object* obj, void* event_info) { WebView* webview = static_cast(data); webview->is_programmatic_navigation_ = false; + webview->target_scroll_x_ = -1; + webview->target_scroll_y_ = -1; Ewk_Error* error = static_cast(event_info); std::string url = ewk_error_url_get(error) ? std::string(ewk_error_url_get(error)) : ""; diff --git a/packages/flutter_inappwebview/tizen/src/webview.h b/packages/flutter_inappwebview/tizen/src/webview.h index 96f913389..6bfe23c59 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.h +++ b/packages/flutter_inappwebview/tizen/src/webview.h @@ -141,6 +141,8 @@ class WebView : public PlatformView { bool is_navigation_cancelled_ = false; std::string committed_url_; std::string pending_navigation_revert_url_; + int32_t target_scroll_x_ = -1; + int32_t target_scroll_y_ = -1; static std::set instances_; static std::mutex instances_mutex_; From a7471ed9fc47f2331fc33d4fe8f9228af9484029 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Wed, 12 Aug 2026 18:31:52 +0900 Subject: [PATCH 4/9] [flutter_inappwebview] Bump flutter_inappwebview_tizen to 0.2.0 --- packages/flutter_inappwebview/CHANGELOG.md | 17 +++++++++++++---- packages/flutter_inappwebview/README.md | 2 +- packages/flutter_inappwebview/pubspec.yaml | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/flutter_inappwebview/CHANGELOG.md b/packages/flutter_inappwebview/CHANGELOG.md index 0ad08baff..6d8b92bc5 100644 --- a/packages/flutter_inappwebview/CHANGELOG.md +++ b/packages/flutter_inappwebview/CHANGELOG.md @@ -1,8 +1,17 @@ -## 0.1.2 +## 0.2.0 -* Update analysis_options.yaml for Flutter 3.47.0. -* Temporarily suppress new analyze issues via analysis_options.yaml rules after the Flutter 3.47.0 upgrade. -* Update the repository URL to use the `main` branch. +- Update analysis_options.yaml for Flutter 3.47.0. +- Temporarily suppress new analyze issues via analysis_options.yaml rules after the Flutter 3.47.0 upgrade. +- Update the repository URL to use the `main` branch. +- Fix `onTitleChanged` to also fire when the page's title changes after the + initial load (e.g. when JavaScript updates `document.title`), instead of + only once when loading finishes. +- Fix a race where `getUrl()` could return the URL of a navigation that was + cancelled via `shouldOverrideUrlLoading`, and skip the + `shouldOverrideUrlLoading` round-trip for app-initiated navigations + (`loadUrl`, `goBack`, `reload`, etc.). +- Fix `scrollBy`/`getScrollX`/`getScrollY` occasionally returning a stale + scroll position right after `scrollTo`/`scrollBy`. ## 0.1.1 diff --git a/packages/flutter_inappwebview/README.md b/packages/flutter_inappwebview/README.md index f932e7e1c..6ecfa54d3 100644 --- a/packages/flutter_inappwebview/README.md +++ b/packages/flutter_inappwebview/README.md @@ -26,7 +26,7 @@ Add the internet privilege to the app manifest: ```yaml dependencies: flutter_inappwebview: ^6.1.5 - flutter_inappwebview_tizen: ^0.1.2 + flutter_inappwebview_tizen: ^0.2.0 ``` ```dart diff --git a/packages/flutter_inappwebview/pubspec.yaml b/packages/flutter_inappwebview/pubspec.yaml index ca70a4725..2af12467f 100644 --- a/packages/flutter_inappwebview/pubspec.yaml +++ b/packages/flutter_inappwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_inappwebview_tizen description: Tizen implementation of the flutter_inappwebview plugin. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/main/packages/flutter_inappwebview -version: 0.1.2 +version: 0.2.0 environment: sdk: ">=3.8.0 <4.0.0" From cf485de7afb3f4ce865d85984038a4311b22411e Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Tue, 21 Jul 2026 12:44:23 +0900 Subject: [PATCH 5/9] [flutter_inappwebview] Add integration tests based on upstream v6.1.5 Add Tizen-compatible test cases derived from the upstream flutter_inappwebview v6.1.5 integration test suite, covering the parts of the API that the Tizen implementation actually supports (the InAppWebView widget/controller and CookieManager.deleteAllCookies). Most of upstream's suite exercises features this plugin does not implement (in-app browser, Chrome Custom Tabs, headless webview, find interaction, service worker, proxy, tracing, process-global config, the local asset-loader server, and most Android/iOS-only settings and callbacks), so those tests don't apply here and were left out. New test cases, alongside the 4 already in the file: - getProgress reports 100 once the page finishes loading - reload reloads the currently displayed page - loadUrl navigates to a new URL - postUrl and loadUrl submit an HTTP POST request body - loadFile loads a bundled asset file - programmatic scroll updates and reports the scroll position - onScrollChanged fires when the scroll position changes - onTitleChanged fires when document.title changes - stopLoading interrupts an in-flight page load - clearAllCache completes without throwing - zoomBy triggers onZoomScaleChanged - onReceivedError reports a host lookup failure / is not raised for a successful load - setSettings applies updated webview settings The new tests reuse the file's existing local HTTP server fixture instead of upstream's live external URLs, so they stay reliable on a TV emulator or device without depending on outside network resources. A small bundled HTML asset was added for the loadFile case. Making the onTitleChanged test pass required fixing a gap in the plugin itself (separate commit): it only reported the title once, right after a page finished loading, and never listened for later title changes such as JavaScript setting document.title. Validated with `flutter-tizen drive` on a Raspberry Pi device (all 17 cases pass). flutter_inappwebview is currently marked disabled for the TV emulator profile in .github/recipe.yaml because of a separate, unrelated crash on WebView disposal there; that is out of scope for this change. The postUrl/loadUrl body assertions poll for the expected text via _waitForCondition instead of reading document.querySelector('p') immediately, since the page's DOM update after a POST/navigation isn't synchronous with the awaited call and the immediate read was occasionally flaky. --- .../assets/test_assets/load_file_test.html | 10 + .../flutter_inappwebview_test.dart | 402 +++++++++++++++++- .../flutter_inappwebview/example/pubspec.yaml | 2 + 3 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 packages/flutter_inappwebview/example/assets/test_assets/load_file_test.html diff --git a/packages/flutter_inappwebview/example/assets/test_assets/load_file_test.html b/packages/flutter_inappwebview/example/assets/test_assets/load_file_test.html new file mode 100644 index 000000000..bb80a8033 --- /dev/null +++ b/packages/flutter_inappwebview/example/assets/test_assets/load_file_test.html @@ -0,0 +1,10 @@ + + + + + Load file test + + +

Loaded from asset

+ + diff --git a/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart b/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart index 58232c234..517cbbc71 100644 --- a/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart +++ b/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart @@ -3,7 +3,9 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; @@ -44,11 +46,13 @@ void main() { late String firstUrl; late String secondUrl; late String blockedUrl; + late String echoPostUrl; + late String slowUrl; setUpAll(() async { server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); unawaited( - server.forEach((HttpRequest request) { + server.forEach((HttpRequest request) async { request.response.headers.contentType = ContentType.html; switch (request.uri.path) { case '/first': @@ -57,18 +61,26 @@ void main() { request.response.write(_htmlPage('Second page')); case '/blocked': request.response.write(_htmlPage('Blocked page')); + case '/echo-post': + final String body = await utf8.decoder.bind(request).join(); + request.response.write('

$body

'); + case '/slow': + await Future.delayed(const Duration(seconds: 5)); + request.response.write(_htmlPage('Slow page')); case '/favicon.ico': request.response.statusCode = HttpStatus.notFound; default: fail('unexpected request: ${request.method} ${request.uri}'); } - request.response.close(); + await request.response.close(); }), ); final String baseUrl = 'http://${server.address.address}:${server.port}'; firstUrl = '$baseUrl/first'; secondUrl = '$baseUrl/second'; blockedUrl = '$baseUrl/blocked'; + echoPostUrl = '$baseUrl/echo-post'; + slowUrl = '$baseUrl/slow'; }); tearDownAll(() => server.close(force: true)); @@ -286,16 +298,378 @@ document.cookie; ); expect(cookieAfter.toString(), isNot(contains('tizen_inappwebview=1'))); }); + + testWidgets('getProgress reports 100 once the page finishes loading', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + addTearDown(loadStops.close); + + final InAppWebViewController controller = await _pumpWebView( + tester, + initialUrl: firstUrl, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + ); + await _waitForValue(loadStops.stream, firstUrl); + + expect(await controller.getProgress(), 100); + }); + + testWidgets('reload reloads the currently displayed page', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + addTearDown(loadStops.close); + + final InAppWebViewController controller = await _pumpWebView( + tester, + initialUrl: firstUrl, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + ); + await _waitForValue(loadStops.stream, firstUrl); + + final Future reloaded = loadStops.stream.first.timeout( + const Duration(seconds: 10), + ); + await controller.reload(); + expect(await reloaded, firstUrl); + }); + + testWidgets('loadUrl navigates to a new URL', (WidgetTester tester) async { + final StreamController loadStops = + StreamController.broadcast(); + addTearDown(loadStops.close); + + final InAppWebViewController controller = await _pumpWebView( + tester, + initialUrl: firstUrl, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + ); + await _waitForValue(loadStops.stream, firstUrl); + + final Future secondLoad = _waitForValue( + loadStops.stream, + secondUrl, + ); + await controller.loadUrl(urlRequest: URLRequest(url: WebUri(secondUrl))); + expect(await secondLoad, secondUrl); + expect((await controller.getUrl()).toString(), secondUrl); + }); + + testWidgets('postUrl and loadUrl submit an HTTP POST request body', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + addTearDown(loadStops.close); + + final InAppWebViewController controller = await _pumpWebView( + tester, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + ); + + final Future firstPost = _waitForValue( + loadStops.stream, + echoPostUrl, + ); + await controller.postUrl( + url: WebUri(echoPostUrl), + postData: Uint8List.fromList(utf8.encode('name=postUrl')), + ); + await firstPost; + expect( + await _waitForCondition( + () => controller.evaluateJavascript( + source: "document.querySelector('p')?.textContent", + ), + (Object? value) => value == 'name=postUrl', + ), + 'name=postUrl', + ); + + final Future secondPost = loadStops.stream.first.timeout( + const Duration(seconds: 10), + ); + await controller.loadUrl( + urlRequest: URLRequest( + url: WebUri(echoPostUrl), + method: 'POST', + body: Uint8List.fromList(utf8.encode('name=loadUrl')), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + ), + ); + expect(await secondPost, echoPostUrl); + expect( + await _waitForCondition( + () => controller.evaluateJavascript( + source: "document.querySelector('p')?.textContent", + ), + (Object? value) => value == 'name=loadUrl', + ), + 'name=loadUrl', + ); + }); + + testWidgets('loadFile loads a bundled asset file', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + addTearDown(loadStops.close); + + final InAppWebViewController controller = await _pumpWebView( + tester, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + ); + + final Future fileLoaded = loadStops.stream.firstWhere( + (String url) => url.endsWith('load_file_test.html'), + ); + await controller.loadFile( + assetFilePath: 'assets/test_assets/load_file_test.html', + ); + await fileLoaded.timeout(const Duration(seconds: 10)); + + expect( + await _waitForCondition( + () => controller.evaluateJavascript(source: "document.title"), + (Object? value) => value == 'Load file test', + ), + 'Load file test', + ); + expect( + await controller.evaluateJavascript( + source: "document.querySelector('h1').textContent", + ), + 'Loaded from asset', + ); + }); + + testWidgets('programmatic scroll updates and reports the scroll position', ( + WidgetTester tester, + ) async { + final InAppWebViewController controller = await _pumpWebView(tester); + await _loadFixture(controller); + + await controller.scrollTo(x: 0, y: 0); + + const int scrollX = 30; + const int scrollY = 40; + await controller.scrollTo(x: scrollX, y: scrollY); + expect(await controller.getScrollX(), scrollX); + expect(await controller.getScrollY(), scrollY); + + await controller.scrollBy(x: scrollX, y: scrollY); + expect(await controller.getScrollX(), scrollX * 2); + expect(await controller.getScrollY(), scrollY * 2); + }); + + testWidgets('onScrollChanged fires when the scroll position changes', ( + WidgetTester tester, + ) async { + final Completer scrollChanged = Completer(); + final InAppWebViewController controller = await _pumpWebView( + tester, + onScrollChanged: (_, int x, int y) { + if (x == 50 && y == 60 && !scrollChanged.isCompleted) { + scrollChanged.complete(); + } + }, + ); + await _loadFixture(controller); + + await controller.scrollTo(x: 50, y: 60); + await scrollChanged.future.timeout(const Duration(seconds: 10)); + }); + + testWidgets('onTitleChanged fires when document.title changes', ( + WidgetTester tester, + ) async { + final Completer titleChanged = Completer(); + final InAppWebViewController controller = await _pumpWebView( + tester, + onTitleChanged: (_, String? title) { + if (title == 'updated title' && !titleChanged.isCompleted) { + titleChanged.complete(); + } + }, + ); + await _loadFixture(controller); + + await controller.evaluateJavascript( + source: "document.title = 'updated title';", + ); + await titleChanged.future.timeout(const Duration(seconds: 10)); + }); + + testWidgets('stopLoading interrupts an in-flight page load', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + addTearDown(loadStops.close); + + await _pumpWebView( + tester, + initialUrl: slowUrl, + onLoadStart: (InAppWebViewController controller, WebUri? url) { + controller.stopLoading(); + }, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + ); + + final Future slowLoad = _waitForValue( + loadStops.stream, + slowUrl, + timeout: const Duration(seconds: 2), + ); + await expectLater(slowLoad, throwsA(isA())); + }); + + testWidgets('clearAllCache completes without throwing', ( + WidgetTester tester, + ) async { + await expectLater( + InAppWebViewController.clearAllCache(includeDiskFiles: true), + completes, + ); + }); + + testWidgets('zoomBy triggers onZoomScaleChanged', ( + WidgetTester tester, + ) async { + final Completer zoomRatio = Completer(); + final InAppWebViewController controller = await _pumpWebView( + tester, + onZoomScaleChanged: (_, double oldScale, double newScale) { + if (!zoomRatio.isCompleted) { + zoomRatio.complete(newScale / oldScale); + } + }, + ); + await _loadFixture(controller); + + await controller.zoomBy(zoomFactor: 2); + expect(await zoomRatio.future.timeout(const Duration(seconds: 10)), 2); + }); + + testWidgets( + 'onReceivedError reports a host lookup failure for an unresolvable URL', + (WidgetTester tester) async { + final Completer receivedError = + Completer(); + + await _pumpWebView( + tester, + initialUrl: 'http://this-domain-does-not-exist.invalid/', + onReceivedError: (_, WebResourceRequest __, WebResourceError error) { + if (!receivedError.isCompleted) { + receivedError.complete(error); + } + }, + ); + + final WebResourceError error = await receivedError.future.timeout( + const Duration(seconds: 10), + ); + expect(error.type, WebResourceErrorType.HOST_LOOKUP); + }, + ); + + testWidgets('onReceivedError is not raised for a successful page load', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + final Completer receivedError = Completer(); + addTearDown(loadStops.close); + + await _pumpWebView( + tester, + initialUrl: firstUrl, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + onReceivedError: (_, WebResourceRequest __, WebResourceError ___) { + receivedError.complete(); + }, + ); + await _waitForValue(loadStops.stream, firstUrl); + + await expectLater( + receivedError.future.timeout(const Duration(seconds: 1)), + throwsA(isA()), + ); + }); + + testWidgets('setSettings applies updated webview settings', ( + WidgetTester tester, + ) async { + final InAppWebViewController controller = await _pumpWebView(tester); + await _loadFixture(controller); + + await expectLater( + controller.setSettings( + settings: InAppWebViewSettings( + javaScriptEnabled: true, + supportZoom: true, + ), + ), + completes, + ); + expect( + await controller.evaluateJavascript( + source: "document.querySelector('h1').textContent", + ), + 'Fixture Page', + ); + }); } Future _pumpWebView( WidgetTester tester, { String initialUrl = 'about:blank', InAppWebViewSettings? initialSettings, + void Function(InAppWebViewController, WebUri?)? onLoadStart, void Function(InAppWebViewController, WebUri?)? onLoadStop, void Function(InAppWebViewController, int)? onProgressChanged, void Function(InAppWebViewController, ConsoleMessage)? onConsoleMessage, void Function(InAppWebViewController, WebUri?, bool?)? onUpdateVisitedHistory, + void Function(InAppWebViewController, int, int)? onScrollChanged, + void Function(InAppWebViewController, String?)? onTitleChanged, + void Function(InAppWebViewController, double, double)? onZoomScaleChanged, + void Function(InAppWebViewController, WebResourceRequest, WebResourceError)? + onReceivedError, Future Function(InAppWebViewController, JsAlertRequest)? onJsAlert, Future Function(InAppWebViewController, JsConfirmRequest)? @@ -319,10 +693,15 @@ Future _pumpWebView( initialSettings: initialSettings, initialUrlRequest: URLRequest(url: WebUri(initialUrl)), onWebViewCreated: controllerCompleter.complete, + onLoadStart: onLoadStart, onLoadStop: onLoadStop, onProgressChanged: onProgressChanged, onConsoleMessage: onConsoleMessage, onUpdateVisitedHistory: onUpdateVisitedHistory, + onScrollChanged: onScrollChanged, + onTitleChanged: onTitleChanged, + onZoomScaleChanged: onZoomScaleChanged, + onReceivedError: onReceivedError, onJsAlert: onJsAlert, onJsConfirm: onJsConfirm, onJsPrompt: onJsPrompt, @@ -383,6 +762,25 @@ Future _waitForValue( return stream.firstWhere((T event) => event == value).timeout(timeout); } +Future _waitForCondition( + Future Function() poll, + bool Function(Object? value) isReady, { + Duration timeout = const Duration(seconds: 10), +}) async { + Object? lastResult; + final DateTime end = DateTime.now().add(timeout); + + while (DateTime.now().isBefore(end)) { + lastResult = await poll(); + if (isReady(lastResult)) { + return lastResult; + } + await Future.delayed(const Duration(milliseconds: 200)); + } + + throw TimeoutException('Condition not met. Last result: $lastResult'); +} + String _htmlPage(String title) { return ''' diff --git a/packages/flutter_inappwebview/example/pubspec.yaml b/packages/flutter_inappwebview/example/pubspec.yaml index ccb0a802b..0bf1815d4 100644 --- a/packages/flutter_inappwebview/example/pubspec.yaml +++ b/packages/flutter_inappwebview/example/pubspec.yaml @@ -31,3 +31,5 @@ dev_dependencies: flutter: uses-material-design: true + assets: + - assets/test_assets/ From 4564294c780df05a8bba2d242e1ca3312c927e91 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Wed, 12 Aug 2026 20:03:43 +0900 Subject: [PATCH 6/9] [flutter_inappwebview] Fix format --- packages/flutter_inappwebview/tizen/src/webview.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc index 4430baa34..5a79920d7 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.cc +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -802,7 +802,7 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, return ewk_view_url_request_set( webview_instance_, url.c_str(), ewk_method, ewk_headers, body.empty() ? nullptr - : reinterpret_cast(body.data())); + : reinterpret_cast(body.data())); }); eina_hash_free(ewk_headers); if (!ret) { @@ -830,8 +830,7 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, const bool ret = NavigateProgrammatically([&] { return ewk_view_url_request_set( webview_instance_, url.c_str(), EWK_HTTP_METHOD_POST, nullptr, - body.empty() ? nullptr - : reinterpret_cast(body.data())); + body.empty() ? nullptr : reinterpret_cast(body.data())); }); if (ret) { result->Success(); @@ -953,7 +952,8 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, int32_t base_y = (target_scroll_y_ >= 0) ? target_scroll_y_ : current_y; target_scroll_x_ = base_x + x; target_scroll_y_ = base_y + y; - ewk_view_scroll_set(webview_instance_, target_scroll_x_, target_scroll_y_); + ewk_view_scroll_set(webview_instance_, target_scroll_x_, + target_scroll_y_); } int32_t new_x = target_scroll_x_; int32_t new_y = target_scroll_y_; From f0b17790dd7301b4316fcc3dde8d35ff87b1aef4 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Thu, 13 Aug 2026 10:32:19 +0900 Subject: [PATCH 7/9] [flutter_inappwebview] Fix stale programmatic-nav flag and scroll target goBack/goForward always reported success to NavigateProgrammatically regardless of whether ewk_view_back()/ewk_view_forward() actually had history to navigate. When called with no history, no navigation policy callback ever fires to clear is_programmatic_navigation_, so the flag leaks into the next user-initiated navigation and incorrectly skips shouldOverrideUrlLoading. Use the EWK calls' own return value instead. getScrollX/getScrollY kept substituting the requested scrollTo/scrollBy target for the actual position until they matched, to mask EWK applying scroll asynchronously. If the requested position is beyond the page's max scroll extent, EWK clamps it and the actual position never matches the target, so out-of-range coordinates were reported indefinitely. Mask only the single read immediately following a scroll instead. --- .../flutter_inappwebview/tizen/src/webview.cc | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc index 5a79920d7..f5d169b58 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.cc +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -880,15 +880,12 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, result->Success(flutter::EncodableValue( static_cast(ewk_view_forward_possible(webview_instance_)))); } else if (method_name == "goBack") { - NavigateProgrammatically([this] { - ewk_view_back(webview_instance_); - return true; - }); + NavigateProgrammatically( + [this] { return static_cast(ewk_view_back(webview_instance_)); }); result->Success(); } else if (method_name == "goForward") { NavigateProgrammatically([this] { - ewk_view_forward(webview_instance_); - return true; + return static_cast(ewk_view_forward(webview_instance_)); }); result->Success(); } else if (method_name == "reload") { @@ -968,18 +965,12 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, int32_t x = 0, y = 0; ewk_view_scroll_pos_get(webview_instance_, &x, &y); if (target_scroll_x_ >= 0) { - if (x == target_scroll_x_) { - target_scroll_x_ = -1; - } else { - x = target_scroll_x_; - } + x = target_scroll_x_; + target_scroll_x_ = -1; } if (target_scroll_y_ >= 0) { - if (y == target_scroll_y_) { - target_scroll_y_ = -1; - } else { - y = target_scroll_y_; - } + y = target_scroll_y_; + target_scroll_y_ = -1; } result->Success( flutter::EncodableValue(method_name == "getScrollX" ? x : y)); From 60da0e120e99a4133353cfd5cb9d34142728c14b Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Thu, 13 Aug 2026 14:31:25 +0900 Subject: [PATCH 8/9] [flutter_inappwebview] Let remote Back reach the delegate; fix stale getUrl after loadData The XF86Back (remote/hardware Back key) handler wrapped ewk_view_back() in NavigateProgrammatically, marking it as a programmatic navigation. OnNavigationPolicy takes the early-accept path for programmatic navigations and never calls shouldOverrideUrlLoading, so apps could not intercept or block a user-initiated Back-key navigation even with useShouldOverrideUrlLoading enabled. Call ewk_view_back() directly so it goes through the normal navigation-policy path, matching goBack() being the only case that should bypass the delegate. is_navigation_cancelled_ (set by StopNavigation() when a delegate cancels a navigation) is only cleared by OnNavigationPolicy. loadData() calls ewk_view_html_string_load(), which never triggers OnNavigationPolicy, so calling loadData() after a cancelled navigation left the flag stuck and getUrl() kept returning the pre-cancellation URL even though new content had loaded. Clear the flag before the html_string_load call. --- packages/flutter_inappwebview/tizen/src/webview.cc | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc index f5d169b58..baca23f55 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.cc +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -505,10 +505,10 @@ bool WebView::SendKey(const char* key, const char* string, const char* compose, if (strcmp(key, "XF86Back") == 0 && !is_down) { if (ewk_view_back_possible(webview_instance_)) { - NavigateProgrammatically([this] { - ewk_view_back(webview_instance_); - return true; - }); + // Not wrapped in NavigateProgrammatically: this is a user-initiated + // navigation (remote Back key), so it must still reach + // shouldOverrideUrlLoading via OnNavigationPolicy. + ewk_view_back(webview_instance_); return true; } return false; @@ -844,6 +844,10 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, return; } GetValueFromEncodableMap(arguments, "baseUrl", &base_url); + // ewk_view_html_string_load() doesn't go through OnNavigationPolicy, so + // a stale cancellation from an earlier navigation would otherwise never + // clear and getUrl() would keep returning the pre-cancellation URL. + is_navigation_cancelled_ = false; NavigateProgrammatically([this, &data, &base_url] { ewk_view_html_string_load(webview_instance_, data.c_str(), base_url.c_str(), nullptr); From 0f0bf8f33c814dfba819b63acec11122eefb4ac7 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Fri, 21 Aug 2026 13:14:05 +0900 Subject: [PATCH 9/9] [flutter_inappwebview] Address PR review comments - Fix getScrollX/getScrollY clearing both axis targets on a single call, which reintroduced the stale-scroll-position bug this PR fixes. - Remove the redundant onTitleChanged emission in OnLoadFinished now that the title,changed callback reports it, avoiding a duplicate event on a normal load. - Skip snapshotting the pre-navigation URL when there's no navigation delegate, since the shouldOverrideUrlLoading race it guards against doesn't apply there. - Rename pending_navigation_revert_url_ to url_before_navigation_ and clarify comments around the suspend/resume and programmatic-nav skip in OnNavigationPolicy/StopNavigation. - Tighten CHANGELOG entries to single-line bullets. --- packages/flutter_inappwebview/CHANGELOG.md | 15 ++--- .../flutter_inappwebview/tizen/src/webview.cc | 61 +++++++++---------- .../flutter_inappwebview/tizen/src/webview.h | 2 +- 3 files changed, 36 insertions(+), 42 deletions(-) diff --git a/packages/flutter_inappwebview/CHANGELOG.md b/packages/flutter_inappwebview/CHANGELOG.md index 6d8b92bc5..ad4962195 100644 --- a/packages/flutter_inappwebview/CHANGELOG.md +++ b/packages/flutter_inappwebview/CHANGELOG.md @@ -1,17 +1,14 @@ ## 0.2.0 +- Fix `onTitleChanged` to fire on title changes after the initial load, not just once at load finish. +- Fix `getUrl()` returning a cancelled navigation's URL, and skip `shouldOverrideUrlLoading` for app-initiated navigations. +- Fix `scrollBy`/`getScrollX`/`getScrollY` occasionally returning a stale scroll position. + +## 0.1.2 + - Update analysis_options.yaml for Flutter 3.47.0. - Temporarily suppress new analyze issues via analysis_options.yaml rules after the Flutter 3.47.0 upgrade. - Update the repository URL to use the `main` branch. -- Fix `onTitleChanged` to also fire when the page's title changes after the - initial load (e.g. when JavaScript updates `document.title`), instead of - only once when loading finishes. -- Fix a race where `getUrl()` could return the URL of a navigation that was - cancelled via `shouldOverrideUrlLoading`, and skip the - `shouldOverrideUrlLoading` round-trip for app-initiated navigations - (`loadUrl`, `goBack`, `reload`, etc.). -- Fix `scrollBy`/`getScrollX`/`getScrollY` occasionally returning a stale - scroll position right after `scrollTo`/`scrollBy`. ## 0.1.1 diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc index baca23f55..db5ec61fb 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.cc +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -308,9 +308,11 @@ void WebView::StopNavigation() { return; } is_navigation_cancelled_ = true; - if (!pending_navigation_revert_url_.empty()) { - committed_url_ = pending_navigation_revert_url_; + if (!url_before_navigation_.empty()) { + committed_url_ = url_before_navigation_; } + // OnNavigationPolicy suspended the view while awaiting this decision; + // ewk_view_stop() has no effect on a suspended view, so resume first. ewk_view_resume(webview_instance_); ewk_view_stop(webview_instance_); } @@ -968,13 +970,16 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, } else if (method_name == "getScrollX" || method_name == "getScrollY") { int32_t x = 0, y = 0; ewk_view_scroll_pos_get(webview_instance_, &x, &y); - if (target_scroll_x_ >= 0) { - x = target_scroll_x_; - target_scroll_x_ = -1; - } - if (target_scroll_y_ >= 0) { - y = target_scroll_y_; - target_scroll_y_ = -1; + if (method_name == "getScrollX") { + if (target_scroll_x_ >= 0) { + x = target_scroll_x_; + target_scroll_x_ = -1; + } + } else { + if (target_scroll_y_ >= 0) { + y = target_scroll_y_; + target_scroll_y_ = -1; + } } result->Success( flutter::EncodableValue(method_name == "getScrollX" ? x : y)); @@ -1096,15 +1101,6 @@ void WebView::OnLoadFinished(void* data, Evas_Object* obj, void* event_info) { flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}}; webview->webview_channel_->InvokeMethod( "onLoadStop", std::make_unique(args)); - - const char* title = ewk_view_title_get(webview->webview_instance_); - if (title) { - flutter::EncodableMap title_args = { - {flutter::EncodableValue("title"), flutter::EncodableValue(title)}}; - webview->webview_channel_->InvokeMethod( - "onTitleChanged", - std::make_unique(title_args)); - } } void WebView::OnProgress(void* data, Evas_Object* obj, void* event_info) { @@ -1168,31 +1164,32 @@ void WebView::OnNavigationPolicy(void* data, Evas_Object* obj, webview->is_navigation_cancelled_ = false; if (webview->is_programmatic_navigation_) { + // Calls the app made directly (loadUrl/goBack/reload/etc.) don't go + // through shouldOverrideUrlLoading; only navigations the page itself + // initiates (link clicks, redirects, hardware Back) do. webview->is_programmatic_navigation_ = false; ewk_policy_decision_use(policy_decision); return; } - // Snapshot the URL EWK is displaying before accepting the navigation - // below. EWK can fire "url,changed" for the new (possibly-to-be-cancelled) - // URL as soon as ewk_policy_decision_use() runs, racing with the async - // shouldOverrideUrlLoading round-trip. StopNavigation() reverts getUrl() - // using this snapshot rather than whatever "url,changed" reported last, so - // that race can't leave getUrl() stuck on a cancelled URL. - const std::string url_before_navigation = - GetViewUrl(webview->webview_instance_); - // Always accept the navigation on its original frame so iframe loads stay - // in their iframe. The view is then suspended while we wait for the Dart - // shouldOverrideUrlLoading response and either resumed (allow) or stopped - // (cancel) by NavigationRequestResult. - ewk_policy_decision_use(policy_decision); + // in their iframe. if (!webview->has_navigation_delegate_) { + ewk_policy_decision_use(policy_decision); return; } - webview->pending_navigation_revert_url_ = url_before_navigation; + // Snapshot the URL EWK is displaying before accepting, since EWK can fire + // "url,changed" for the new (possibly-to-be-cancelled) URL as soon as + // ewk_policy_decision_use() runs below. + const std::string url_before_navigation = + GetViewUrl(webview->webview_instance_); + ewk_policy_decision_use(policy_decision); + webview->url_before_navigation_ = url_before_navigation; + // The view is then suspended while we wait for the Dart + // shouldOverrideUrlLoading response and either resumed (allow) or stopped + // (cancel) by NavigationRequestResult. const char* url_cstr = ewk_policy_decision_url_get(policy_decision); const std::string url = url_cstr ? std::string(url_cstr) : std::string(); ewk_view_suspend(webview->webview_instance_); diff --git a/packages/flutter_inappwebview/tizen/src/webview.h b/packages/flutter_inappwebview/tizen/src/webview.h index 6bfe23c59..1bb10d814 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.h +++ b/packages/flutter_inappwebview/tizen/src/webview.h @@ -140,7 +140,7 @@ class WebView : public PlatformView { bool is_programmatic_navigation_ = false; bool is_navigation_cancelled_ = false; std::string committed_url_; - std::string pending_navigation_revert_url_; + std::string url_before_navigation_; int32_t target_scroll_x_ = -1; int32_t target_scroll_y_ = -1;